Tutorial

Most state machines start as a sequence of phases in your head: drive a request, wait for ack, latch the data, deassert. Writing that out as always_ff blocks and case statements buries the sequence inside boilerplate, making it harder to reason about control flow or make changes. Cocoa lets you write the sequence directly — the transpiler hands back the FSM you'd have written by hand, with no hardware overhead.

Cocoa isn't high-level synthesis. You're in full control of scheduling and state; it just offers an easier way to describe control flow, without leaving SystemVerilog. The body of a cocoa module lives inside an initial block — written the same way you would in a simulation testbench.

01

Square wave

A synthesizable square wave is two states that toggle on every clock. The left panel is a cocoa coroutine you would write directly; the right what the compiler will generate.

Coroutines are modelled as initial processes that drive outputs and wait for events. Since a state machine cycles forever, the process must either loop with forever or idle after completion.

Combinational outputs are driven with blocking assignments and work identically to always_comb blocks. Each @(posedge clk) transitions to a new state; the lines above it drive the previous state's outputs. When the clock ticks, control moves past the @ and the next chunk "runs".

cocoa.sv
1module square_wave (
2 input logic clk,
3 input logic rst,
4 output logic q
5);
6 initial forever begin
7 // Reset state.
8 q = 1'b1;
9 @(posedge clk);
10 
11 q = 1'b0;
12 @(posedge clk);
13 end
14endmodule
generated.sv
1module square_wave (
2 input logic clk,
3 input logic rst,
4 output logic q
5);
6 
7 typedef enum logic [0:0] { S0, S1 } state_t;
8 state_t state, next_state;
9 
10 always_comb begin
11 unique case (state)
12 S0: begin
13 // Reset state.
14 q = 1'b1;
15 next_state = S1;
16 end
17 S1: begin
18 q = 1'b0;
19 next_state = S0;
20 end
21 default: begin
22 // Reset state.
23 q = 1'b1;
24 next_state = S0;
25 end
26 endcase
27 end
28 
29 always_ff @(posedge clk) begin
30 if (rst) begin
31 state <= S0;
32 end else begin
33 state <= next_state;
34 end
35 end
36 
37endmodule
02

Adjusting the duty cycle

To make the wave 75% duty cycle, the state machine should cycle every four clocks and hold o high for three of them. In the cocoa source we just add more @(posedge clk); statements — each one inserts a new state.

Notice we don't reassign q = 1'b1 in the new states. Outputs are held across states until reassigned; the transpiler tracks held outputs and drives them combinationally in every state. No latch is inferred.

cocoa.sv
1module duty_75 (
2 input logic clk,
3 input logic rst,
4 output logic q
5);
6 initial forever begin
7 q = 1'b1;
8 @(posedge clk);
9 @(posedge clk);
10 @(posedge clk);
11 
12 q = 1'b0;
13 @(posedge clk);
14 end
15endmodule
generated.sv
1module duty_75 (
2 input logic clk,
3 input logic rst,
4 output logic q
5);
6 
7 typedef enum logic [1:0] { S0, S1, S2, S3 } state_t;
8 state_t state, next_state;
9 
10 always_comb begin
11 unique case (state)
12 S0: begin
13 q = 1'b1;
14 next_state = S1;
15 end
16 S1: begin
17 q = 1'b1;
18 next_state = S2;
19 end
20 S2: begin
21 q = 1'b1;
22 next_state = S3;
23 end
24 S3: begin
25 q = 1'b0;
26 next_state = S0;
27 end
28 default: begin
29 q = 1'b1;
30 next_state = S0;
31 end
32 endcase
33 end
34 
35 always_ff @(posedge clk) begin
36 if (rst) begin
37 state <= S0;
38 end else begin
39 state <= next_state;
40 end
41 end
42 
43endmodule

Held outputs are propagated combinationally, not latched. Inputs that need to survive a clock edge — covered later — do require a flop.

03

Waiting on a request

State machines rarely advance unconditionally. A guarded event — @(posedge clk iff cond) — only fires when the condition is true on the clock edge. In source terms, the process suspends in the current state until the guard is satisfied.

cocoa.sv
1module wait_for_request (
2 input logic clk,
3 input logic rst,
4 input logic req,
5 output logic ack
6);
7 initial forever begin
8 // Idle until a request arrives.
9 ack = 1'b0;
10 @(posedge clk iff req);
11 
12 // Acknowledge for one cycle.
13 ack = 1'b1;
14 @(posedge clk);
15 end
16endmodule
generated.sv
1module wait_for_request (
2 input logic clk,
3 input logic rst,
4 input logic req,
5 output logic ack
6);
7 
8 typedef enum logic [0:0] { S0, S1 } state_t;
9 state_t state, next_state;
10 
11 always_comb begin
12 unique case (state)
13 S0: begin
14 // Idle until a request arrives.
15 ack = 1'b0;
16 if (req) next_state = S1;
17 else next_state = S0;
18 end
19 S1: begin
20 // Acknowledge for one cycle.
21 ack = 1'b1;
22 next_state = S0;
23 end
24 default: begin
25 // Idle until a request arrives.
26 ack = 1'b0;
27 next_state = S0;
28 end
29 endcase
30 end
31 
32 always_ff @(posedge clk) begin
33 if (rst) begin
34 state <= S0;
35 end else begin
36 state <= next_state;
37 end
38 end
39 
40endmodule
04

Capture, then emit

Sample data_in when valid_in arrives, hold it one cycle, then drive the captured byte out with valid_out high.

data_q is written in one state and read in the next. Cocoa notices the cross-edge read and turns it into a flop in the generated module. Locals that are only touched within a single state stay as combinational temporaries; no flop is inferred.

cocoa.sv
1module capture_emit (
2 input logic clk,
3 input logic rst,
4 input logic valid_in,
5 input logic [7:0] data_in,
6 output logic valid_out,
7 output logic [7:0] data_out
8);
9 logic [7:0] data_q;
10 
11 initial forever begin
12 valid_out = 1'b0;
13 data_out = 8'h00;
14 @(posedge clk iff valid_in);
15 
16 data_q = data_in;
17 @(posedge clk);
18 
19 valid_out = 1'b1;
20 data_out = data_q;
21 @(posedge clk);
22 end
23endmodule
generated.sv
1module capture_emit (
2 input logic clk,
3 input logic rst,
4 input logic valid_in,
5 input logic [7:0] data_in,
6 output logic valid_out,
7 output logic [7:0] data_out
8);
9 
10 typedef enum logic [1:0] { S0, S1, S2 } state_t;
11 state_t state, next_state;
12 logic [7:0] data_q;
13 
14 always_comb begin
15 unique case (state)
16 S0: begin
17 valid_out = 1'b0;
18 data_out = 8'h00;
19 if (valid_in) next_state = S1;
20 else next_state = S0;
21 end
22 S1: begin
23 valid_out = 1'b0;
24 data_out = 8'h00;
25 next_state = S2;
26 end
27 S2: begin
28 valid_out = 1'b1;
29 data_out = data_q;
30 next_state = S0;
31 end
32 default: begin
33 valid_out = 1'b0;
34 data_out = 8'h00;
35 next_state = S0;
36 end
37 endcase
38 end
39 
40 always_ff @(posedge clk) begin
41 if (rst) begin
42 state <= S0;
43 data_q <= '0;
44 end else begin
45 state <= next_state;
46 unique case (state)
47 S0: if (valid_in) begin
48 data_q <= data_in;
49 end
50 S1: begin end
51 S2: begin end
52 default: begin end
53 endcase
54 end
55 end
56 
57endmodule

A local that lives across a clock edge becomes a flop automatically. There's no need to declare it separately or write a manual always_ff block.

05

Branching outputs

After go, drive a different value on q depending on mode. An if/else between two clock waits selects what gets driven during the second state.

This is the simplest branch form: an if/else with no @ inside. Both branches drive the same signals to different values and the state's output expression becomes a ternary. Branches can contain their own @ — see the reference for the diverging form — but the no-wait variant is enough here.

cocoa.sv
1module branch_q (
2 input logic clk,
3 input logic rst,
4 input logic go,
5 input logic mode,
6 output logic [7:0] q
7);
8 initial forever begin
9 q = 8'd0;
10 @(posedge clk iff go);
11 
12 if (mode) q = 8'hAA;
13 else q = 8'h55;
14 @(posedge clk);
15 end
16endmodule
generated.sv
1module branch_q (
2 input logic clk,
3 input logic rst,
4 input logic go,
5 input logic mode,
6 output logic [7:0] q
7);
8 
9 typedef enum logic [0:0] { S0, S1 } state_t;
10 state_t state, next_state;
11 
12 always_comb begin
13 unique case (state)
14 S0: begin
15 q = 8'd0;
16 if (go) next_state = S1;
17 else next_state = S0;
18 end
19 S1: begin
20 if (mode) q = 8'hAA;
21 else q = 8'h55;
22 next_state = S0;
23 end
24 default: begin
25 q = 8'd0;
26 next_state = S0;
27 end
28 endcase
29 end
30 
31 always_ff @(posedge clk) begin
32 if (rst) begin
33 state <= S0;
34 end else begin
35 state <= next_state;
36 end
37 end
38 
39endmodule
06

N-cycle delay

After go, wait N clocks then pulse done. Useful for fixed-latency sequences — config-register write windows, bus turnaround timing, and the like.

repeat (N) @(posedge clk); collapses to a single FSM state with an auto-generated counter (cyc0_q in the output). The state self-loops while the counter is below N-1 and exits when it hits the bound. No unrolling: the cost is one state plus a $clog2(N)-bit counter regardless of N.

cocoa.sv
1module delay_n #(
2 parameter int N = 8
3) (
4 input logic clk,
5 input logic rst,
6 input logic go,
7 output logic done
8);
9 initial forever begin
10 done = 1'b0;
11 @(posedge clk iff go);
12 
13 repeat (N) @(posedge clk);
14 
15 done = 1'b1;
16 @(posedge clk);
17 end
18endmodule
generated.sv
1module delay_n #(
2 parameter int N = 8
3) (
4 input logic clk,
5 input logic rst,
6 input logic go,
7 output logic done
8);
9 
10 typedef enum logic [1:0] { S0, S1, S2 } state_t;
11 state_t state, next_state;
12 logic [$clog2(N)-1:0] cyc0_q;
13 
14 always_comb begin
15 unique case (state)
16 S0: begin
17 done = 1'b0;
18 if (go) next_state = S1;
19 else next_state = S0;
20 end
21 S1: begin
22 done = 1'b0;
23 if (cyc0_q < (N) - 1) next_state = S1;
24 else next_state = S2;
25 end
26 S2: begin
27 done = 1'b1;
28 next_state = S0;
29 end
30 default: begin
31 done = 1'b0;
32 next_state = S0;
33 end
34 endcase
35 end
36 
37 always_ff @(posedge clk) begin
38 if (rst) begin
39 state <= S0;
40 cyc0_q <= '0;
41 end else begin
42 state <= next_state;
43 unique case (state)
44 S0: begin end
45 S1: begin
46 if (cyc0_q < (N) - 1) cyc0_q <= cyc0_q + 1;
47 else begin
48 cyc0_q <= '0;
49 end
50 end
51 S2: begin end
52 default: begin end
53 endcase
54 end
55 end
56 
57endmodule

N can be a parameter; the counter width follows from it.

07

Burst write

After go, drive N consecutive bus writes — w_addr walks 0..N-1 and w_data mirrors the index. The kind of inner loop you'd write to initialise a small register file.

The counter i is a module-level local, so the transpiler exposes it as a real flop you can reference in the body — here driving both w_addr and w_data. The loop body must contain a clock wait on every path, which is why @(posedge clk) sits inside the begin/end.

cocoa.sv
1module burst_write #(
2 parameter int N = 4
3) (
4 input logic clk,
5 input logic rst,
6 input logic go,
7 output logic w_req,
8 output logic [7:0] w_addr,
9 output logic [7:0] w_data
10);
11 logic [7:0] i;
12 
13 initial forever begin
14 w_req = 1'b0;
15 @(posedge clk iff go);
16 
17 for (i = 0; i < N; i = i + 1) begin
18 w_req = 1'b1;
19 w_addr = i;
20 w_data = i;
21 @(posedge clk);
22 end
23 end
24endmodule
generated.sv
1module burst_write #(
2 parameter int N = 4
3) (
4 input logic clk,
5 input logic rst,
6 input logic go,
7 output logic w_req,
8 output logic [7:0] w_addr,
9 output logic [7:0] w_data
10);
11 
12 typedef enum logic [0:0] { S0, S1 } state_t;
13 state_t state, next_state;
14 logic [7:0] i;
15 
16 always_comb begin
17 unique case (state)
18 S0: begin
19 w_addr = i;
20 w_data = i;
21 w_req = 1'b0;
22 if (go) next_state = S1;
23 else next_state = S0;
24 end
25 S1: begin
26 w_req = 1'b1;
27 w_addr = i;
28 w_data = i;
29 if (i < (N) - 1) next_state = S1;
30 else next_state = S0;
31 end
32 default: begin
33 w_addr = i;
34 w_data = i;
35 w_req = 1'b0;
36 next_state = S0;
37 end
38 endcase
39 end
40 
41 always_ff @(posedge clk) begin
42 if (rst) begin
43 state <= S0;
44 i <= 0;
45 end else begin
46 state <= next_state;
47 unique case (state)
48 S0: begin end
49 S1: begin
50 if (i < (N) - 1) i <= i + 1;
51 else begin
52 i <= 0;
53 end
54 end
55 default: begin end
56 endcase
57 end
58 end
59 
60endmodule
08

Busy wait

After start, spin while busy_in is high. When it clears, pulse ready for one cycle.

while (cond) @ produces a state that self-loops while the condition holds and falls through when it clears. No counter is generated — the exit condition isn't bounded by an iteration count. To bound it, write while (cond && cyc < TIMEOUT) and pair it with a counter.

cocoa.sv
1module busy_wait (
2 input logic clk,
3 input logic rst,
4 input logic start,
5 input logic busy_in,
6 output logic ready
7);
8 initial forever begin
9 ready = 1'b0;
10 @(posedge clk iff start);
11 
12 while (busy_in) @(posedge clk);
13 
14 ready = 1'b1;
15 @(posedge clk);
16 end
17endmodule
generated.sv
1module busy_wait (
2 input logic clk,
3 input logic rst,
4 input logic start,
5 input logic busy_in,
6 output logic ready
7);
8 
9 typedef enum logic [1:0] { S0, S1, S2 } state_t;
10 state_t state, next_state;
11 
12 always_comb begin
13 unique case (state)
14 S0: begin
15 ready = 1'b0;
16 if (start) next_state = S1;
17 else next_state = S0;
18 end
19 S1: begin
20 ready = 1'b0;
21 if (busy_in) next_state = S1;
22 else next_state = S2;
23 end
24 S2: begin
25 ready = 1'b1;
26 next_state = S0;
27 end
28 default: begin
29 ready = 1'b0;
30 next_state = S0;
31 end
32 endcase
33 end
34 
35 always_ff @(posedge clk) begin
36 if (rst) begin
37 state <= S0;
38 end else begin
39 state <= next_state;
40 end
41 end
42 
43endmodule
09

Tasks for reuse

A bus write is two cycles: drive bus_req, bus_addr, and bus_data for one clock, then deassert. Doing it twice inline duplicates four lines. Wrap it in a task automatic and call it twice.

Tasks are inlined at compile time with arguments substituted in. The two call sites in the generated FSM appear as states S1..S4 — the same code laid out back to back with the literal arguments in place. The trailing forever @(posedge clk); compiles to a final self-looping idle state that holds the bus in its quiet pattern after both writes finish. Tasks accept input formals only, and the call graph must be acyclic.

cocoa.sv
1module task_write (
2 input logic clk,
3 input logic rst,
4 output logic bus_req,
5 output logic [7:0] bus_addr,
6 output logic [31:0] bus_data
7);
8 // In sub-FSM mode, a task cannot write a module output port
9 // directly — it must declare an `output` formal so ownership of
10 // the port stays with the parent FSM. The caller passes the port
11 // as the actual; parent's always_comb drives the port from the
12 // task's arg_out flop while in WAIT state.
13 task automatic write_reg(
14 output logic bus_req_o,
15 output logic [7:0] bus_addr_o,
16 output logic [31:0] bus_data_o,
17 input logic [7:0] addr,
18 input logic [31:0] data
19 );
20 bus_req_o = 1'b1;
21 bus_addr_o = addr;
22 bus_data_o = data;
23 @(posedge clk);
24 bus_req_o = 1'b0;
25 bus_addr_o = 8'd0;
26 bus_data_o = 32'd0;
27 @(posedge clk);
28 endtask
29 
30 initial begin
31 bus_req = 1'b0;
32 bus_addr = 8'd0;
33 bus_data = 32'd0;
34 @(posedge clk);
35 
36 write_reg(bus_req, bus_addr, bus_data, 8'h10, 32'hDEAD_BEEF);
37 write_reg(bus_req, bus_addr, bus_data, 8'h14, 32'h0000_0001);
38 
39 // Initialisation done — park on the bus idle pattern forever.
40 forever @(posedge clk);
41 end
42endmodule
generated.sv
1module task_write (
2 input logic clk,
3 input logic rst,
4 output logic bus_req,
5 output logic [7:0] bus_addr,
6 output logic [31:0] bus_data
7);
8 
9 typedef enum logic [2:0] { S0, S1, S2, S3, S4, S5 } state_t;
10 state_t state, next_state;
11 typedef enum logic [2:0] { S0_write_reg_c0, S1_write_reg_c0, S2_write_reg_c0, S3_write_reg_c0, S4_write_reg_c0 } state_write_reg_c0_t;
12 state_write_reg_c0_t state_write_reg_c0, next_state_write_reg_c0;
13 logic [7:0] arg_in_addr_write_reg_c0_q;
14 logic [31:0] arg_in_data_write_reg_c0_q;
15 logic arg_out_bus_req_o_write_reg_c0_q;
16 logic [7:0] arg_out_bus_addr_o_write_reg_c0_q;
17 logic [31:0] arg_out_bus_data_o_write_reg_c0_q;
18 logic launch_write_reg_c0;
19 logic done_write_reg_c0;
20 
21 assign launch_write_reg_c0 = (state == S1) || (state == S3);
22 assign done_write_reg_c0 = (state_write_reg_c0 == S4_write_reg_c0);
23 
24 always_comb begin
25 unique case (state)
26 S0: begin
27 bus_req = 1'b0;
28 bus_addr = 8'd0;
29 bus_data = 32'd0;
30 next_state = S1;
31 end
32 S1: begin
33 bus_req = 1'b0;
34 bus_addr = 8'd0;
35 bus_data = 32'd0;
36 next_state = S2;
37 end
38 S2: begin
39 bus_req = arg_out_bus_req_o_write_reg_c0_q;
40 bus_addr = arg_out_bus_addr_o_write_reg_c0_q;
41 bus_data = arg_out_bus_data_o_write_reg_c0_q;
42 if (done_write_reg_c0) next_state = S3;
43 else next_state = S2;
44 end
45 S3: begin
46 bus_req = arg_out_bus_req_o_write_reg_c0_q;
47 bus_addr = arg_out_bus_addr_o_write_reg_c0_q;
48 bus_data = arg_out_bus_data_o_write_reg_c0_q;
49 next_state = S4;
50 end
51 S4: begin
52 bus_req = arg_out_bus_req_o_write_reg_c0_q;
53 bus_addr = arg_out_bus_addr_o_write_reg_c0_q;
54 bus_data = arg_out_bus_data_o_write_reg_c0_q;
55 if (done_write_reg_c0) next_state = S5;
56 else next_state = S4;
57 end
58 S5: begin
59 bus_req = arg_out_bus_req_o_write_reg_c0_q;
60 bus_addr = arg_out_bus_addr_o_write_reg_c0_q;
61 bus_data = arg_out_bus_data_o_write_reg_c0_q;
62 next_state = S5;
63 end
64 default: begin
65 bus_req = 1'b0;
66 bus_addr = 8'd0;
67 bus_data = 32'd0;
68 next_state = S0;
69 end
70 endcase
71 end
72 
73 always_ff @(posedge clk) begin
74 if (rst) begin
75 state <= S0;
76 arg_in_addr_write_reg_c0_q <= '0;
77 arg_in_data_write_reg_c0_q <= '0;
78 end else begin
79 state <= next_state;
80 unique case (state)
81 S0: begin end
82 S1: begin
83 arg_in_addr_write_reg_c0_q <= 8'h10;
84 arg_in_data_write_reg_c0_q <= 32'hDEAD_BEEF;
85 end
86 S2: begin end
87 S3: begin
88 arg_in_addr_write_reg_c0_q <= 8'h14;
89 arg_in_data_write_reg_c0_q <= 32'h0000_0001;
90 end
91 S4: begin end
92 S5: begin end
93 default: begin end
94 endcase
95 end
96 end
97 
98 always_comb begin
99 unique case (state_write_reg_c0)
100 S0_write_reg_c0: begin
101 if (launch_write_reg_c0) next_state_write_reg_c0 = S1_write_reg_c0;
102 else next_state_write_reg_c0 = S0_write_reg_c0;
103 end
104 S1_write_reg_c0: begin
105 next_state_write_reg_c0 = S2_write_reg_c0;
106 end
107 S2_write_reg_c0: begin
108 next_state_write_reg_c0 = S3_write_reg_c0;
109 end
110 S3_write_reg_c0: begin
111 next_state_write_reg_c0 = S4_write_reg_c0;
112 end
113 S4_write_reg_c0: begin
114 next_state_write_reg_c0 = S0_write_reg_c0;
115 end
116 default: begin
117 next_state_write_reg_c0 = S0_write_reg_c0;
118 end
119 endcase
120 end
121 
122 always_ff @(posedge clk) begin
123 if (rst) begin
124 state_write_reg_c0 <= S0_write_reg_c0;
125 arg_out_bus_req_o_write_reg_c0_q <= '0;
126 arg_out_bus_addr_o_write_reg_c0_q <= '0;
127 arg_out_bus_data_o_write_reg_c0_q <= '0;
128 end else begin
129 state_write_reg_c0 <= next_state_write_reg_c0;
130 unique case (state_write_reg_c0)
131 S0_write_reg_c0: begin end
132 S1_write_reg_c0: begin
133 arg_out_bus_req_o_write_reg_c0_q <= 1'b1;
134 arg_out_bus_addr_o_write_reg_c0_q <= arg_in_addr_write_reg_c0_q;
135 arg_out_bus_data_o_write_reg_c0_q <= arg_in_data_write_reg_c0_q;
136 end
137 S2_write_reg_c0: begin
138 arg_out_bus_req_o_write_reg_c0_q <= 1'b0;
139 arg_out_bus_addr_o_write_reg_c0_q <= 8'd0;
140 arg_out_bus_data_o_write_reg_c0_q <= 32'd0;
141 end
142 S3_write_reg_c0: begin end
143 S4_write_reg_c0: begin end
144 default: begin end
145 endcase
146 end
147 end
148 
149endmodule
10

Boot init sequence

After boot asserts, write four (addr, data) pairs to the config bus and pulse init_done when the writes finish. The bring-up sequence you'd otherwise build out of a hand-coded state machine plus a counter and a ROM lookup.

The source stays linear top-to-bottom; the generated FSM grows one block per call. Cost scales with the number of phases, not with surrounding control logic.

cocoa.sv
1module boot_init (
2 input logic clk,
3 input logic rst,
4 input logic boot,
5 output logic cfg_req,
6 output logic [7:0] cfg_addr,
7 output logic [31:0] cfg_data,
8 output logic init_done
9);
10 // Sub-FSM mode requires module output ports to be passed as
11 // `output` formals — the parent retains ownership of the port,
12 // its always_comb forwards the task's arg_out flop while in WAIT.
13 task automatic write_cfg(
14 output logic cfg_req_o,
15 output logic [7:0] cfg_addr_o,
16 output logic [31:0] cfg_data_o,
17 input logic [7:0] addr,
18 input logic [31:0] data
19 );
20 cfg_req_o = 1'b1;
21 cfg_addr_o = addr;
22 cfg_data_o = data;
23 @(posedge clk);
24 cfg_req_o = 1'b0;
25 cfg_addr_o = 8'd0;
26 cfg_data_o = 32'd0;
27 @(posedge clk);
28 endtask
29 
30 initial forever begin
31 init_done = 1'b0;
32 cfg_req = 1'b0;
33 cfg_addr = 8'd0;
34 cfg_data = 32'd0;
35 @(posedge clk iff boot);
36 
37 write_cfg(cfg_req, cfg_addr, cfg_data, 8'h00, 32'h0000_0001);
38 write_cfg(cfg_req, cfg_addr, cfg_data, 8'h04, 32'h0000_00FF);
39 write_cfg(cfg_req, cfg_addr, cfg_data, 8'h08, 32'hCAFE_0000);
40 write_cfg(cfg_req, cfg_addr, cfg_data, 8'h0C, 32'h0000_0010);
41 
42 init_done = 1'b1;
43 @(posedge clk);
44 end
45endmodule
generated.sv
1module boot_init (
2 input logic clk,
3 input logic rst,
4 input logic boot,
5 output logic cfg_req,
6 output logic [7:0] cfg_addr,
7 output logic [31:0] cfg_data,
8 output logic init_done
9);
10 
11 typedef enum logic [3:0] { S0, S1, S2, S3, S4, S5, S6, S7, S8, S9 } state_t;
12 state_t state, next_state;
13 typedef enum logic [2:0] { S0_write_cfg_c0, S1_write_cfg_c0, S2_write_cfg_c0, S3_write_cfg_c0, S4_write_cfg_c0 } state_write_cfg_c0_t;
14 state_write_cfg_c0_t state_write_cfg_c0, next_state_write_cfg_c0;
15 logic [7:0] arg_in_addr_write_cfg_c0_q;
16 logic [31:0] arg_in_data_write_cfg_c0_q;
17 logic arg_out_cfg_req_o_write_cfg_c0_q;
18 logic [7:0] arg_out_cfg_addr_o_write_cfg_c0_q;
19 logic [31:0] arg_out_cfg_data_o_write_cfg_c0_q;
20 logic launch_write_cfg_c0;
21 logic done_write_cfg_c0;
22 
23 assign launch_write_cfg_c0 = (state == S1) || (state == S3) || (state == S5) || (state == S7);
24 assign done_write_cfg_c0 = (state_write_cfg_c0 == S4_write_cfg_c0);
25 
26 always_comb begin
27 unique case (state)
28 S0: begin
29 init_done = 1'b0;
30 cfg_req = 1'b0;
31 cfg_addr = 8'd0;
32 cfg_data = 32'd0;
33 if (boot) next_state = S1;
34 else next_state = S0;
35 end
36 S1: begin
37 cfg_req = 1'b0;
38 cfg_addr = 8'd0;
39 cfg_data = 32'd0;
40 init_done = 1'b0;
41 next_state = S2;
42 end
43 S2: begin
44 init_done = 1'b0;
45 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
46 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
47 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
48 if (done_write_cfg_c0) next_state = S3;
49 else next_state = S2;
50 end
51 S3: begin
52 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
53 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
54 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
55 init_done = 1'b0;
56 next_state = S4;
57 end
58 S4: begin
59 init_done = 1'b0;
60 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
61 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
62 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
63 if (done_write_cfg_c0) next_state = S5;
64 else next_state = S4;
65 end
66 S5: begin
67 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
68 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
69 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
70 init_done = 1'b0;
71 next_state = S6;
72 end
73 S6: begin
74 init_done = 1'b0;
75 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
76 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
77 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
78 if (done_write_cfg_c0) next_state = S7;
79 else next_state = S6;
80 end
81 S7: begin
82 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
83 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
84 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
85 init_done = 1'b0;
86 next_state = S8;
87 end
88 S8: begin
89 init_done = 1'b0;
90 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
91 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
92 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
93 if (done_write_cfg_c0) next_state = S9;
94 else next_state = S8;
95 end
96 S9: begin
97 cfg_req = arg_out_cfg_req_o_write_cfg_c0_q;
98 cfg_addr = arg_out_cfg_addr_o_write_cfg_c0_q;
99 cfg_data = arg_out_cfg_data_o_write_cfg_c0_q;
100 init_done = 1'b1;
101 next_state = S0;
102 end
103 default: begin
104 init_done = 1'b0;
105 cfg_req = 1'b0;
106 cfg_addr = 8'd0;
107 cfg_data = 32'd0;
108 next_state = S0;
109 end
110 endcase
111 end
112 
113 always_ff @(posedge clk) begin
114 if (rst) begin
115 state <= S0;
116 arg_in_addr_write_cfg_c0_q <= '0;
117 arg_in_data_write_cfg_c0_q <= '0;
118 end else begin
119 state <= next_state;
120 unique case (state)
121 S0: begin end
122 S1: begin
123 arg_in_addr_write_cfg_c0_q <= 8'h00;
124 arg_in_data_write_cfg_c0_q <= 32'h0000_0001;
125 end
126 S2: begin end
127 S3: begin
128 arg_in_addr_write_cfg_c0_q <= 8'h04;
129 arg_in_data_write_cfg_c0_q <= 32'h0000_00FF;
130 end
131 S4: begin end
132 S5: begin
133 arg_in_addr_write_cfg_c0_q <= 8'h08;
134 arg_in_data_write_cfg_c0_q <= 32'hCAFE_0000;
135 end
136 S6: begin end
137 S7: begin
138 arg_in_addr_write_cfg_c0_q <= 8'h0C;
139 arg_in_data_write_cfg_c0_q <= 32'h0000_0010;
140 end
141 S8: begin end
142 S9: begin end
143 default: begin end
144 endcase
145 end
146 end
147 
148 always_comb begin
149 unique case (state_write_cfg_c0)
150 S0_write_cfg_c0: begin
151 if (launch_write_cfg_c0) next_state_write_cfg_c0 = S1_write_cfg_c0;
152 else next_state_write_cfg_c0 = S0_write_cfg_c0;
153 end
154 S1_write_cfg_c0: begin
155 next_state_write_cfg_c0 = S2_write_cfg_c0;
156 end
157 S2_write_cfg_c0: begin
158 next_state_write_cfg_c0 = S3_write_cfg_c0;
159 end
160 S3_write_cfg_c0: begin
161 next_state_write_cfg_c0 = S4_write_cfg_c0;
162 end
163 S4_write_cfg_c0: begin
164 next_state_write_cfg_c0 = S0_write_cfg_c0;
165 end
166 default: begin
167 next_state_write_cfg_c0 = S0_write_cfg_c0;
168 end
169 endcase
170 end
171 
172 always_ff @(posedge clk) begin
173 if (rst) begin
174 state_write_cfg_c0 <= S0_write_cfg_c0;
175 arg_out_cfg_req_o_write_cfg_c0_q <= '0;
176 arg_out_cfg_addr_o_write_cfg_c0_q <= '0;
177 arg_out_cfg_data_o_write_cfg_c0_q <= '0;
178 end else begin
179 state_write_cfg_c0 <= next_state_write_cfg_c0;
180 unique case (state_write_cfg_c0)
181 S0_write_cfg_c0: begin end
182 S1_write_cfg_c0: begin
183 arg_out_cfg_req_o_write_cfg_c0_q <= 1'b1;
184 arg_out_cfg_addr_o_write_cfg_c0_q <= arg_in_addr_write_cfg_c0_q;
185 arg_out_cfg_data_o_write_cfg_c0_q <= arg_in_data_write_cfg_c0_q;
186 end
187 S2_write_cfg_c0: begin
188 arg_out_cfg_req_o_write_cfg_c0_q <= 1'b0;
189 arg_out_cfg_addr_o_write_cfg_c0_q <= 8'd0;
190 arg_out_cfg_data_o_write_cfg_c0_q <= 32'd0;
191 end
192 S3_write_cfg_c0: begin end
193 S4_write_cfg_c0: begin end
194 default: begin end
195 endcase
196 end
197 end
198 
199endmodule

Init sequences are the second place coroutines pay off, after protocols. The hand-written equivalent needs an explicit counter, an output mux, and a termination test. Here the writes appear in the order they happen on the bus.

11

UART transmitter

Send a UART frame: start bit (0), eight data bits LSB-first, stop bit (1). Each bit holds for CLKS_PER_BIT cycles. The body reads top-to-bottom as exactly that — send_bit(0), eight calls for data_reg[0] through data_reg[7], then send_bit(1).

send_bit is called ten times across the frame. Naively inlining would give ten distinct states with nearly identical contents. The transpiler collapses repeated call sites of the same task into a single state controlled by a small program-counter register pc_q. The generated FSM has just two states — S0 (idle) and S1 (sending). S1's tx output is a case (pc_q) mux selecting the bit for each call site. The bit-period delay (repeat (CLKS_PER_BIT) @) lives inside send_bit and produces the inner counter cyc0_q.

That collapse is why the generated module fits on screen. Without it, it's roughly 5× the size.

cocoa.sv
1module uart_tx #(
2 parameter int CLKS_PER_BIT = 87
3) (
4 input logic clk,
5 input logic rst,
6 input logic go,
7 input logic [7:0] data_in,
8 output logic tx
9);
10 logic [7:0] data_reg;
11 
12 // `tx_o` is an output formal — the parent forwards the task's
13 // arg_out flop to the actual `tx` port while in WAIT.
14 task automatic send_bit(output logic tx_o, input logic b);
15 tx_o = b;
16 repeat (CLKS_PER_BIT) @(posedge clk);
17 endtask
18 
19 initial forever begin
20 tx = 1'b1;
21 @(posedge clk iff go);
22 data_reg = data_in;
23 
24 send_bit(tx, 1'b0); // start bit
25 send_bit(tx, data_reg[0]);
26 send_bit(tx, data_reg[1]);
27 send_bit(tx, data_reg[2]);
28 send_bit(tx, data_reg[3]);
29 send_bit(tx, data_reg[4]);
30 send_bit(tx, data_reg[5]);
31 send_bit(tx, data_reg[6]);
32 send_bit(tx, data_reg[7]);
33 send_bit(tx, 1'b1); // stop bit
34 end
35endmodule
generated.sv
1module uart_tx #(
2 parameter int CLKS_PER_BIT = 87
3) (
4 input logic clk,
5 input logic rst,
6 input logic go,
7 input logic [7:0] data_in,
8 output logic tx
9);
10 
11 typedef enum logic [4:0] { S0, S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, S12, S13, S14, S15, S16, S17, S18, S19, S20 } state_t;
12 state_t state, next_state;
13 typedef enum logic [1:0] { S0_send_bit_c0, S1_send_bit_c0, S2_send_bit_c0, S3_send_bit_c0 } state_send_bit_c0_t;
14 state_send_bit_c0_t state_send_bit_c0, next_state_send_bit_c0;
15 logic [7:0] data_reg;
16 logic arg_in_b_send_bit_c0_q;
17 logic [$clog2(CLKS_PER_BIT)-1:0] cyc0_q;
18 logic arg_out_tx_o_send_bit_c0_q;
19 logic launch_send_bit_c0;
20 logic done_send_bit_c0;
21 
22 assign launch_send_bit_c0 = (state == S1) || (state == S3) || (state == S5) || (state == S7) || (state == S9) || (state == S11) || (state == S13) || (state == S15) || (state == S17) || (state == S19);
23 assign done_send_bit_c0 = (state_send_bit_c0 == S3_send_bit_c0);
24 
25 always_comb begin
26 unique case (state)
27 S0: begin
28 tx = 1'b1;
29 if (go) next_state = S1;
30 else next_state = S0;
31 end
32 S1: begin
33 tx = 1'b1;
34 next_state = S2;
35 end
36 S2: begin
37 tx = arg_out_tx_o_send_bit_c0_q;
38 if (done_send_bit_c0) next_state = S3;
39 else next_state = S2;
40 end
41 S3: begin
42 tx = arg_out_tx_o_send_bit_c0_q;
43 next_state = S4;
44 end
45 S4: begin
46 tx = arg_out_tx_o_send_bit_c0_q;
47 if (done_send_bit_c0) next_state = S5;
48 else next_state = S4;
49 end
50 S5: begin
51 tx = arg_out_tx_o_send_bit_c0_q;
52 next_state = S6;
53 end
54 S6: begin
55 tx = arg_out_tx_o_send_bit_c0_q;
56 if (done_send_bit_c0) next_state = S7;
57 else next_state = S6;
58 end
59 S7: begin
60 tx = arg_out_tx_o_send_bit_c0_q;
61 next_state = S8;
62 end
63 S8: begin
64 tx = arg_out_tx_o_send_bit_c0_q;
65 if (done_send_bit_c0) next_state = S9;
66 else next_state = S8;
67 end
68 S9: begin
69 tx = arg_out_tx_o_send_bit_c0_q;
70 next_state = S10;
71 end
72 S10: begin
73 tx = arg_out_tx_o_send_bit_c0_q;
74 if (done_send_bit_c0) next_state = S11;
75 else next_state = S10;
76 end
77 S11: begin
78 tx = arg_out_tx_o_send_bit_c0_q;
79 next_state = S12;
80 end
81 S12: begin
82 tx = arg_out_tx_o_send_bit_c0_q;
83 if (done_send_bit_c0) next_state = S13;
84 else next_state = S12;
85 end
86 S13: begin
87 tx = arg_out_tx_o_send_bit_c0_q;
88 next_state = S14;
89 end
90 S14: begin
91 tx = arg_out_tx_o_send_bit_c0_q;
92 if (done_send_bit_c0) next_state = S15;
93 else next_state = S14;
94 end
95 S15: begin
96 tx = arg_out_tx_o_send_bit_c0_q;
97 next_state = S16;
98 end
99 S16: begin
100 tx = arg_out_tx_o_send_bit_c0_q;
101 if (done_send_bit_c0) next_state = S17;
102 else next_state = S16;
103 end
104 S17: begin
105 tx = arg_out_tx_o_send_bit_c0_q;
106 next_state = S18;
107 end
108 S18: begin
109 tx = arg_out_tx_o_send_bit_c0_q;
110 if (done_send_bit_c0) next_state = S19;
111 else next_state = S18;
112 end
113 S19: begin
114 tx = arg_out_tx_o_send_bit_c0_q;
115 next_state = S20;
116 end
117 S20: begin
118 tx = arg_out_tx_o_send_bit_c0_q;
119 if (done_send_bit_c0) next_state = S0;
120 else next_state = S20;
121 end
122 default: begin
123 tx = 1'b1;
124 next_state = S0;
125 end
126 endcase
127 end
128 
129 always_ff @(posedge clk) begin
130 if (rst) begin
131 state <= S0;
132 data_reg <= '0;
133 arg_in_b_send_bit_c0_q <= '0;
134 end else begin
135 state <= next_state;
136 unique case (state)
137 S0: if (go) begin
138 data_reg <= data_in;
139 end
140 S1: begin
141 arg_in_b_send_bit_c0_q <= 1'b0;
142 end
143 S2: begin end
144 S3: begin
145 arg_in_b_send_bit_c0_q <= data_reg[0];
146 end
147 S4: begin end
148 S5: begin
149 arg_in_b_send_bit_c0_q <= data_reg[1];
150 end
151 S6: begin end
152 S7: begin
153 arg_in_b_send_bit_c0_q <= data_reg[2];
154 end
155 S8: begin end
156 S9: begin
157 arg_in_b_send_bit_c0_q <= data_reg[3];
158 end
159 S10: begin end
160 S11: begin
161 arg_in_b_send_bit_c0_q <= data_reg[4];
162 end
163 S12: begin end
164 S13: begin
165 arg_in_b_send_bit_c0_q <= data_reg[5];
166 end
167 S14: begin end
168 S15: begin
169 arg_in_b_send_bit_c0_q <= data_reg[6];
170 end
171 S16: begin end
172 S17: begin
173 arg_in_b_send_bit_c0_q <= data_reg[7];
174 end
175 S18: begin end
176 S19: begin
177 arg_in_b_send_bit_c0_q <= 1'b1;
178 end
179 S20: begin end
180 default: begin end
181 endcase
182 end
183 end
184 
185 always_comb begin
186 unique case (state_send_bit_c0)
187 S0_send_bit_c0: begin
188 if (launch_send_bit_c0) next_state_send_bit_c0 = S1_send_bit_c0;
189 else next_state_send_bit_c0 = S0_send_bit_c0;
190 end
191 S1_send_bit_c0: begin
192 next_state_send_bit_c0 = S2_send_bit_c0;
193 end
194 S2_send_bit_c0: begin
195 if (cyc0_q < (CLKS_PER_BIT) - 1) next_state_send_bit_c0 = S2_send_bit_c0;
196 else next_state_send_bit_c0 = S3_send_bit_c0;
197 end
198 S3_send_bit_c0: begin
199 next_state_send_bit_c0 = S0_send_bit_c0;
200 end
201 default: begin
202 next_state_send_bit_c0 = S0_send_bit_c0;
203 end
204 endcase
205 end
206 
207 always_ff @(posedge clk) begin
208 if (rst) begin
209 state_send_bit_c0 <= S0_send_bit_c0;
210 cyc0_q <= '0;
211 arg_out_tx_o_send_bit_c0_q <= '0;
212 end else begin
213 state_send_bit_c0 <= next_state_send_bit_c0;
214 unique case (state_send_bit_c0)
215 S0_send_bit_c0: begin end
216 S1_send_bit_c0: begin
217 arg_out_tx_o_send_bit_c0_q <= arg_in_b_send_bit_c0_q;
218 end
219 S2_send_bit_c0: begin
220 if (cyc0_q < (CLKS_PER_BIT) - 1) cyc0_q <= cyc0_q + 1;
221 else begin
222 cyc0_q <= '0;
223 end
224 end
225 S3_send_bit_c0: begin end
226 default: begin end
227 endcase
228 end
229 end
230 
231endmodule

Further reading.

  • tests/uart/ — adds an RX coroutine running concurrently. Two independent initial blocks share clk and rst; each compiles to its own state machine.
  • tests/axi_lite_write/ — the full AXI-Lite write handshake. Same set of constructs at protocol scale.