Skip to content

Commit ddfc605

Browse files
authored
FiniteStateMachine upgrades (#593)
1 parent 71a0700 commit ddfc605

File tree

7 files changed

+152
-57
lines changed

7 files changed

+152
-57
lines changed

.github/workflows/general.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ jobs:
3434

3535
- name: Setup Dart
3636
uses: dart-lang/setup-dart@v1
37-
with:
38-
sdk: 3.6.2 # downgrade pending https://github.com/dart-lang/dartdoc/issues/3996
3937

4038
- name: Setup Node
4139
uses: actions/setup-node@v4

doc/user_guide/_docs/A15-fsm.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,55 @@ FiniteStateMachine<LightStates>(
113113
);
114114
```
115115

116+
Notice, that in all states, one of the lights is red. We could simplify the FSM by removing all the actions to make a light red and setting up a default in `setupActions` for the FSM. That way, every state would default to both lights being red, and we'd only need to specify states when they are non-red. For example:
117+
118+
```dart
119+
final states = <State<LightStates>>[
120+
State(LightStates.northFlowing, events: {
121+
TrafficPresence.isEastActive(traffic): LightStates.northSlowing,
122+
}, actions: [
123+
northLight < LightColor.green.value,
124+
]),
125+
State(
126+
LightStates.northSlowing,
127+
events: {},
128+
defaultNextState: LightStates.eastFlowing,
129+
actions: [
130+
northLight < LightColor.yellow.value,
131+
],
132+
),
133+
State(
134+
LightStates.eastFlowing,
135+
events: {
136+
TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing,
137+
},
138+
actions: [
139+
eastLight < LightColor.green.value,
140+
],
141+
),
142+
State(
143+
LightStates.eastSlowing,
144+
events: {},
145+
defaultNextState: LightStates.northFlowing,
146+
actions: [
147+
eastLight < LightColor.yellow.value,
148+
],
149+
),
150+
];
151+
152+
FiniteStateMachine<LightStates>(
153+
clk,
154+
reset,
155+
LightStates.northFlowing,
156+
states,
157+
setupActions: [
158+
// by default, lights should be red
159+
northLight < LightColor.red.value,
160+
eastLight < LightColor.red.value,
161+
],
162+
);
163+
```
164+
116165
This state machine is now functional and synthesizable into SystemVerilog!
117166

118167
You can even generate a mermaid diagram for the state machine using the [`generateDiagram`](https://intel.github.io/rohd/rohd/FiniteStateMachine/generateDiagram.html) API.

lib/src/finite_state_machine.dart

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ class FiniteStateMachine<StateIdentifier> {
4646
return _stateValueLookup[_stateLookup[id]];
4747
}
4848

49+
/// A [Map] from the [StateIdentifier]s to the internal index used to
50+
/// represent that state in the state machine.
51+
late final Map<StateIdentifier, int> stateIndexLookup = UnmodifiableMapView(
52+
_stateValueLookup.map((key, value) => MapEntry(key.identifier, value)));
53+
4954
/// The clock signal to the FSM (when only single-triggered). Otherwise, the
5055
/// first clock.
5156
///
@@ -68,6 +73,11 @@ class FiniteStateMachine<StateIdentifier> {
6873
/// bus.
6974
final Logic currentState;
7075

76+
/// A [List] of [Conditional] actions to perform at the beginning of the
77+
/// evaluation of actions for the [FiniteStateMachine]. This is useful for
78+
/// things like setting up default values for signals across all states.
79+
final List<Conditional> setupActions;
80+
7181
/// The next state of the FSM.
7282
///
7383
/// Use [getStateIndex] to map from a [StateIdentifier] to the value on this
@@ -78,7 +88,7 @@ class FiniteStateMachine<StateIdentifier> {
7888
static int _logBase(num x, num base) => (log(x) / log(base)).ceil();
7989

8090
/// Width of the state.
81-
final int _stateWidth;
91+
final int stateWidth;
8292

8393
/// If `true`, the [reset] signal is asynchronous.
8494
final bool asyncReset;
@@ -92,17 +102,24 @@ class FiniteStateMachine<StateIdentifier> {
92102
StateIdentifier resetState,
93103
List<State<StateIdentifier>> states, {
94104
bool asyncReset = false,
95-
}) : this.multi([clk], reset, resetState, states, asyncReset: asyncReset);
105+
List<Conditional> setupActions = const [],
106+
}) : this.multi([clk], reset, resetState, states,
107+
asyncReset: asyncReset, setupActions: setupActions);
96108

97109
/// Creates an finite state machine for the specified list of [_states], with
98110
/// an initial state of [resetState] (when [reset] is high) and transitions on
99111
/// positive edges of any of [_clks].
100112
///
101113
/// If [asyncReset] is `true`, the [reset] signal is asynchronous.
102114
FiniteStateMachine.multi(
103-
this._clks, this.reset, this.resetState, this._states,
104-
{this.asyncReset = false})
105-
: _stateWidth = _logBase(_states.length, 2),
115+
this._clks,
116+
this.reset,
117+
this.resetState,
118+
this._states, {
119+
this.asyncReset = false,
120+
List<Conditional> setupActions = const [],
121+
}) : setupActions = List.unmodifiable(setupActions),
122+
stateWidth = _logBase(_states.length, 2),
106123
currentState =
107124
Logic(name: 'currentState', width: _logBase(_states.length, 2)),
108125
nextState =
@@ -116,26 +133,29 @@ class FiniteStateMachine<StateIdentifier> {
116133
}
117134

118135
Combinational([
136+
...setupActions,
119137
Case(
120138
currentState,
121139
_states
122140
.map((state) => CaseItem(
123-
Const(_stateValueLookup[state], width: _stateWidth), [
124-
...state.actions,
125-
Case(
126-
Const(1),
127-
state.events.entries
128-
.map((entry) => CaseItem(entry.key, [
129-
nextState <
130-
_stateValueLookup[
131-
_stateLookup[entry.value]]
132-
]))
133-
.toList(growable: false),
134-
conditionalType: state.conditionalType,
135-
defaultItem: [
136-
nextState < getStateIndex(state.defaultNextState),
137-
])
138-
]))
141+
Const(_stateValueLookup[state], width: stateWidth)
142+
.named(state.identifier.toString()),
143+
[
144+
...state.actions,
145+
Case(
146+
Const(1),
147+
state.events.entries
148+
.map((entry) => CaseItem(entry.key, [
149+
nextState <
150+
_stateValueLookup[
151+
_stateLookup[entry.value]]
152+
]))
153+
.toList(growable: false),
154+
conditionalType: state.conditionalType,
155+
defaultItem: [
156+
nextState < getStateIndex(state.defaultNextState),
157+
])
158+
]))
139159
.toList(growable: false),
140160
conditionalType: ConditionalType.unique,
141161
defaultItem: [

lib/src/utilities/simcompare.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,15 @@ abstract class SimCompare {
160160
}
161161

162162
for (final vector in vectors) {
163-
Simulator.registerAction(timestamp, () {
163+
Simulator.registerAction(timestamp, () async {
164164
for (final signalName in vector.inputValues.keys) {
165165
final value = vector.inputValues[signalName];
166166
(module.tryInput(signalName) ?? getIoInputDriver(signalName))
167167
.put(value);
168168
}
169169

170170
if (enableChecking) {
171-
Simulator.postTick.first.then((value) {
171+
unawaited(Simulator.postTick.first.then((value) {
172172
for (final signalName in vector.expectedOutputValues.keys) {
173173
final value = vector.expectedOutputValues[signalName];
174174
final o =
@@ -207,7 +207,7 @@ abstract class SimCompare {
207207
(Object err, StackTrace stackTrace) {
208208
Simulator.throwException(err as Exception, stackTrace);
209209
},
210-
);
210+
));
211211
}
212212
});
213213
timestamp += Vector._period;

test/fsm_test.dart

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -122,15 +122,13 @@ class TrafficTestModule extends Module {
122122
TrafficPresence.isEastActive(traffic): LightStates.northSlowing,
123123
}, actions: [
124124
northLight < LightColor.green.value,
125-
eastLight < LightColor.red.value,
126125
]),
127126
State(
128127
LightStates.northSlowing,
129128
events: {},
130129
defaultNextState: LightStates.eastFlowing,
131130
actions: [
132131
northLight < LightColor.yellow.value,
133-
eastLight < LightColor.red.value,
134132
],
135133
),
136134
State(
@@ -139,7 +137,6 @@ class TrafficTestModule extends Module {
139137
TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing,
140138
},
141139
actions: [
142-
northLight < LightColor.red.value,
143140
eastLight < LightColor.green.value,
144141
],
145142
),
@@ -148,7 +145,6 @@ class TrafficTestModule extends Module {
148145
events: {},
149146
defaultNextState: LightStates.northFlowing,
150147
actions: [
151-
northLight < LightColor.red.value,
152148
eastLight < LightColor.yellow.value,
153149
],
154150
),
@@ -159,6 +155,11 @@ class TrafficTestModule extends Module {
159155
reset,
160156
LightStates.northFlowing,
161157
states,
158+
setupActions: [
159+
// by default, lights should be red
160+
northLight < LightColor.red.value,
161+
eastLight < LightColor.red.value,
162+
],
162163
);
163164

164165
if (!kIsWeb) {
@@ -179,23 +180,49 @@ void main() {
179180
});
180181

181182
test('zero-out receivers in default case', () async {
182-
final pipem = TestModule(Logic(), Logic(), Logic());
183-
await pipem.build();
183+
final mod = TestModule(Logic(), Logic(), Logic());
184+
await mod.build();
184185

185-
final sv = pipem.generateSynth();
186+
final sv = mod.generateSynth();
186187

187188
expect(sv, contains("b = 1'h0;"));
188189
});
189190

190191
test('conditional type is used', () async {
191-
final pipem = TestModule(Logic(), Logic(), Logic());
192-
await pipem.build();
192+
final mod = TestModule(Logic(), Logic(), Logic());
193+
await mod.build();
193194

194-
final sv = pipem.generateSynth();
195+
final sv = mod.generateSynth();
195196

196197
expect(sv, contains('priority case'));
197198
});
198199

200+
test('label name included in generated SV', () async {
201+
final mod = TestModule(Logic(), Logic(), Logic());
202+
await mod.build();
203+
204+
final sv = mod.generateSynth();
205+
206+
expect(sv, contains('MyStates_state1 : begin'));
207+
});
208+
209+
test('state value lookup is correct', () async {
210+
final mod = DefaultStateFsmMod(Logic());
211+
await mod.build();
212+
213+
expect(mod._fsm.stateWidth, 2);
214+
215+
expect(mod._fsm.stateIndexLookup.length, MyStates.values.length);
216+
expect(
217+
MyStates.values.every((e) => mod._fsm.stateIndexLookup.containsKey(e)),
218+
isTrue);
219+
for (var i = 0; i < MyStates.values.length; i++) {
220+
final stateEnum = MyStates.values[i];
221+
expect(mod._fsm.getStateIndex(stateEnum), i);
222+
expect(mod._fsm.stateIndexLookup[stateEnum], i);
223+
}
224+
});
225+
199226
group('fsm validation', () {
200227
test('duplicate state identifiers throws exception', () {
201228
expect(
@@ -231,42 +258,42 @@ void main() {
231258

232259
group('simcompare', () {
233260
test('simple fsm', () async {
234-
final pipem = TestModule(Logic(), Logic(), Logic());
261+
final mod = TestModule(Logic(), Logic(), Logic());
235262

236-
await pipem.build();
263+
await mod.build();
237264

238265
final vectors = [
239266
Vector({'reset': 1, 'a': 0, 'c': 0}, {}),
240267
Vector({'reset': 0}, {'b': 0}),
241268
Vector({}, {'b': 1}),
242269
Vector({'c': 1}, {'b': 0}),
243270
];
244-
await SimCompare.checkFunctionalVector(pipem, vectors);
245-
SimCompare.checkIverilogVector(pipem, vectors);
271+
await SimCompare.checkFunctionalVector(mod, vectors);
272+
SimCompare.checkIverilogVector(mod, vectors);
246273

247274
verifyMermaidStateDiagram(_simpleFSMPath);
248275
});
249276

250277
test('simple fsm async reset', () async {
251-
final pipem =
278+
final mod =
252279
TestModule(Logic(), Logic(), Logic(), testingAsyncReset: true);
253280

254-
await pipem.build();
281+
await mod.build();
255282

256283
final vectors = [
257284
Vector({'reset': 0, 'a': 0, 'c': 0}, {}),
258285
Vector({'reset': 1}, {'b': 0}),
259286
];
260-
await SimCompare.checkFunctionalVector(pipem, vectors);
261-
SimCompare.checkIverilogVector(pipem, vectors);
287+
await SimCompare.checkFunctionalVector(mod, vectors);
288+
SimCompare.checkIverilogVector(mod, vectors);
262289

263290
verifyMermaidStateDiagram(_simpleFSMPath);
264291
});
265292

266293
test('default next state fsm', () async {
267-
final pipem = DefaultStateFsmMod(Logic());
294+
final mod = DefaultStateFsmMod(Logic());
268295

269-
await pipem.build();
296+
await mod.build();
270297

271298
final vectors = [
272299
Vector({'reset': 1}, {}),
@@ -275,12 +302,12 @@ void main() {
275302
Vector({'reset': 0}, {'b': 4}),
276303
Vector({'reset': 0}, {'b': 4}),
277304
];
278-
await SimCompare.checkFunctionalVector(pipem, vectors);
279-
SimCompare.checkIverilogVector(pipem, vectors);
305+
await SimCompare.checkFunctionalVector(mod, vectors);
306+
SimCompare.checkIverilogVector(mod, vectors);
280307

281308
if (!kIsWeb) {
282309
const fsmPath = '$_tmpDir/default_next_state_fsm.md';
283-
pipem._fsm.generateDiagram(outputPath: fsmPath);
310+
mod._fsm.generateDiagram(outputPath: fsmPath);
284311

285312
final mermaid = File(fsmPath).readAsStringSync();
286313
expect(mermaid, contains('state2'));
@@ -293,8 +320,8 @@ void main() {
293320
});
294321

295322
test('traffic light fsm', () async {
296-
final pipem = TrafficTestModule(Logic(width: 2), Logic());
297-
await pipem.build();
323+
final mod = TrafficTestModule(Logic(width: 2), Logic());
324+
await mod.build();
298325

299326
final vectors = [
300327
Vector({'reset': 1, 'traffic': 00}, {}),
@@ -315,8 +342,8 @@ void main() {
315342
'eastLight': LightColor.green.value
316343
})
317344
];
318-
await SimCompare.checkFunctionalVector(pipem, vectors);
319-
SimCompare.checkIverilogVector(pipem, vectors);
345+
await SimCompare.checkFunctionalVector(mod, vectors);
346+
SimCompare.checkIverilogVector(mod, vectors);
320347

321348
verifyMermaidStateDiagram(_trafficFSMPath);
322349
});

0 commit comments

Comments
 (0)