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 o
5);
6 initial forever begin
7 o = 1'b1;
8 @(posedge clk);
9 o = 1'b0;
10 @(posedge clk);
11 end
12endmodule
generated.sv
1module square_wave (
2 input logic clk,
3 input logic rst,
4 output logic o
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 o = 1'b1;
14 next_state = S1;
15 end
16 S1: begin
17 o = 1'b0;
18 next_state = S0;
19 end
20 default: begin
21 o = 1'b1;
22 next_state = S0;
23 end
24 endcase
25 end
26 
27 always_ff @(posedge clk) begin
28 if (rst) begin
29 state <= S0;
30 end else begin
31 state <= next_state;
32 end
33 end
34 
35endmodule
02

Adjusting the duty cycle

To make the wave 75% duty, 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 o = 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 o
5);
6 initial forever begin
7 o = 1'b1;
8 @(posedge clk);
9 @(posedge clk);
10 @(posedge clk);
11 o = 1'b0;
12 @(posedge clk);
13 end
14endmodule
generated.sv
1module duty_75 (
2 input logic clk,
3 input logic rst,
4 output logic o
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 o = 1'b1;
14 next_state = S1;
15 end
16 S1: begin
17 o = 1'b1;
18 next_state = S2;
19 end
20 S2: begin
21 o = 1'b1;
22 next_state = S3;
23 end
24 S3: begin
25 o = 1'b0;
26 next_state = S0;
27 end
28 default: begin
29 o = 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. 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 task automatic write_reg(
9 input logic [7:0] addr,
10 input logic [31:0] data
11 );
12 bus_req = 1'b1;
13 bus_addr = addr;
14 bus_data = data;
15 @(posedge clk);
16 bus_req = 1'b0;
17 bus_addr = 8'd0;
18 bus_data = 32'd0;
19 @(posedge clk);
20 endtask
21 
22 initial begin
23 bus_req = 1'b0;
24 bus_addr = 8'd0;
25 bus_data = 32'd0;
26 @(posedge clk);
27 
28 write_reg(8'h10, 32'hDEAD_BEEF);
29 write_reg(8'h14, 32'h0000_0001);
30 end
31endmodule
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 } state_t;
10 state_t state, next_state;
11 
12 always_comb begin
13 unique case (state)
14 S0: begin
15 bus_req = 1'b0;
16 bus_addr = 8'd0;
17 bus_data = 32'd0;
18 next_state = S1;
19 end
20 S1: begin
21 bus_req = 1'b1;
22 bus_addr = 8'h10;
23 bus_data = 32'hDEAD_BEEF;
24 next_state = S2;
25 end
26 S2: begin
27 bus_req = 1'b0;
28 bus_addr = 8'd0;
29 bus_data = 32'd0;
30 next_state = S3;
31 end
32 S3: begin
33 bus_req = 1'b1;
34 bus_addr = 8'h14;
35 bus_data = 32'h0000_0001;
36 next_state = S4;
37 end
38 S4: begin
39 bus_req = 1'b0;
40 bus_addr = 8'd0;
41 bus_data = 32'd0;
42 next_state = S4;
43 end
44 default: begin
45 bus_req = 1'b0;
46 bus_addr = 8'd0;
47 bus_data = 32'd0;
48 next_state = S0;
49 end
50 endcase
51 end
52 
53 always_ff @(posedge clk) begin
54 if (rst) begin
55 state <= S0;
56 end else begin
57 state <= next_state;
58 end
59 end
60 
61endmodule
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 task automatic write_cfg(
11 input logic [7:0] addr,
12 input logic [31:0] data
13 );
14 cfg_req = 1'b1;
15 cfg_addr = addr;
16 cfg_data = data;
17 @(posedge clk);
18 cfg_req = 1'b0;
19 cfg_addr = 8'd0;
20 cfg_data = 32'd0;
21 @(posedge clk);
22 endtask
23 
24 initial forever begin
25 init_done = 1'b0;
26 cfg_req = 1'b0;
27 cfg_addr = 8'd0;
28 cfg_data = 32'd0;
29 @(posedge clk iff boot);
30 
31 write_cfg(8'h00, 32'h0000_0001);
32 write_cfg(8'h04, 32'h0000_00FF);
33 write_cfg(8'h08, 32'hCAFE_0000);
34 write_cfg(8'h0C, 32'h0000_0010);
35 
36 init_done = 1'b1;
37 @(posedge clk);
38 end
39endmodule
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 
14 always_comb begin
15 unique case (state)
16 S0: begin
17 init_done = 1'b0;
18 cfg_req = 1'b0;
19 cfg_addr = 8'd0;
20 cfg_data = 32'd0;
21 if (boot) next_state = S1;
22 else next_state = S0;
23 end
24 S1: begin
25 init_done = 1'b0;
26 cfg_req = 1'b1;
27 cfg_addr = 8'h00;
28 cfg_data = 32'h0000_0001;
29 next_state = S2;
30 end
31 S2: begin
32 init_done = 1'b0;
33 cfg_req = 1'b0;
34 cfg_addr = 8'd0;
35 cfg_data = 32'd0;
36 next_state = S3;
37 end
38 S3: begin
39 init_done = 1'b0;
40 cfg_req = 1'b1;
41 cfg_addr = 8'h04;
42 cfg_data = 32'h0000_00FF;
43 next_state = S4;
44 end
45 S4: begin
46 init_done = 1'b0;
47 cfg_req = 1'b0;
48 cfg_addr = 8'd0;
49 cfg_data = 32'd0;
50 next_state = S5;
51 end
52 S5: begin
53 init_done = 1'b0;
54 cfg_req = 1'b1;
55 cfg_addr = 8'h08;
56 cfg_data = 32'hCAFE_0000;
57 next_state = S6;
58 end
59 S6: begin
60 init_done = 1'b0;
61 cfg_req = 1'b0;
62 cfg_addr = 8'd0;
63 cfg_data = 32'd0;
64 next_state = S7;
65 end
66 S7: begin
67 init_done = 1'b0;
68 cfg_req = 1'b1;
69 cfg_addr = 8'h0C;
70 cfg_data = 32'h0000_0010;
71 next_state = S8;
72 end
73 S8: begin
74 init_done = 1'b0;
75 cfg_req = 1'b0;
76 cfg_addr = 8'd0;
77 cfg_data = 32'd0;
78 next_state = S9;
79 end
80 S9: begin
81 cfg_req = 1'b0;
82 cfg_addr = 8'd0;
83 cfg_data = 32'd0;
84 init_done = 1'b1;
85 next_state = S0;
86 end
87 default: begin
88 init_done = 1'b0;
89 cfg_req = 1'b0;
90 cfg_addr = 8'd0;
91 cfg_data = 32'd0;
92 next_state = S0;
93 end
94 endcase
95 end
96 
97 always_ff @(posedge clk) begin
98 if (rst) begin
99 state <= S0;
100 end else begin
101 state <= next_state;
102 end
103 end
104 
105endmodule

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 task automatic send_bit(input logic b);
13 tx = b;
14 repeat (CLKS_PER_BIT) @(posedge clk);
15 endtask
16 
17 initial forever begin
18 tx = 1'b1;
19 @(posedge clk iff go);
20 data_reg = data_in;
21 
22 send_bit(1'b0); // start bit
23 send_bit(data_reg[0]);
24 send_bit(data_reg[1]);
25 send_bit(data_reg[2]);
26 send_bit(data_reg[3]);
27 send_bit(data_reg[4]);
28 send_bit(data_reg[5]);
29 send_bit(data_reg[6]);
30 send_bit(data_reg[7]);
31 send_bit(1'b1); // stop bit
32 end
33endmodule
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 [0:0] { S0, S1 } state_t;
12 state_t state, next_state;
13 logic [7:0] data_reg;
14 logic [$clog2(CLKS_PER_BIT)-1:0] cyc0_q;
15 logic [3:0] pc_q;
16 
17 always_comb begin
18 unique case (state)
19 S0: begin
20 tx = 1'b1;
21 if (go) next_state = S1;
22 else next_state = S0;
23 end
24 S1: begin
25 case (pc_q)
26 4'd0: tx = 1'b0;
27 4'd1: tx = data_reg[0];
28 4'd2: tx = data_reg[1];
29 4'd3: tx = data_reg[2];
30 4'd4: tx = data_reg[3];
31 4'd5: tx = data_reg[4];
32 4'd6: tx = data_reg[5];
33 4'd7: tx = data_reg[6];
34 4'd8: tx = data_reg[7];
35 4'd9: tx = 1'b1;
36 default: tx = 1'b0;
37 endcase
38 if (cyc0_q < (CLKS_PER_BIT) - 1) next_state = S1;
39 else if (pc_q < (10) - 1) next_state = S1;
40 else next_state = S0;
41 end
42 default: begin
43 tx = 1'b1;
44 next_state = S0;
45 end
46 endcase
47 end
48 
49 always_ff @(posedge clk) begin
50 if (rst) begin
51 state <= S0;
52 data_reg <= '0;
53 cyc0_q <= '0;
54 pc_q <= '0;
55 end else begin
56 state <= next_state;
57 unique case (state)
58 S0: if (go) begin
59 data_reg <= data_in;
60 end
61 S1: begin
62 if (cyc0_q < (CLKS_PER_BIT) - 1) cyc0_q <= cyc0_q + 1;
63 else begin
64 cyc0_q <= '0;
65 if (pc_q < (10) - 1) pc_q <= pc_q + 1;
66 else begin
67 pc_q <= '0;
68 end
69 end
70 end
71 default: begin end
72 endcase
73 end
74 end
75 
76endmodule

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.