Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/general.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ jobs:

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

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
49 changes: 49 additions & 0 deletions doc/user_guide/_docs/A15-fsm.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,55 @@ FiniteStateMachine<LightStates>(
);
```

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:

```dart
final states = <State<LightStates>>[
State(LightStates.northFlowing, events: {
TrafficPresence.isEastActive(traffic): LightStates.northSlowing,
}, actions: [
northLight < LightColor.green.value,
]),
State(
LightStates.northSlowing,
events: {},
defaultNextState: LightStates.eastFlowing,
actions: [
northLight < LightColor.yellow.value,
],
),
State(
LightStates.eastFlowing,
events: {
TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing,
},
actions: [
eastLight < LightColor.green.value,
],
),
State(
LightStates.eastSlowing,
events: {},
defaultNextState: LightStates.northFlowing,
actions: [
eastLight < LightColor.yellow.value,
],
),
];

FiniteStateMachine<LightStates>(
clk,
reset,
LightStates.northFlowing,
states,
setupActions: [
// by default, lights should be red
northLight < LightColor.red.value,
eastLight < LightColor.red.value,
],
);
```

This state machine is now functional and synthesizable into SystemVerilog!

You can even generate a mermaid diagram for the state machine using the [`generateDiagram`](https://intel.github.io/rohd/rohd/FiniteStateMachine/generateDiagram.html) API.
Expand Down
62 changes: 41 additions & 21 deletions lib/src/finite_state_machine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ class FiniteStateMachine<StateIdentifier> {
return _stateValueLookup[_stateLookup[id]];
}

/// A [Map] from the [StateIdentifier]s to the internal index used to
/// represent that state in the state machine.
late final Map<StateIdentifier, int> stateIndexLookup = UnmodifiableMapView(
_stateValueLookup.map((key, value) => MapEntry(key.identifier, value)));

/// The clock signal to the FSM (when only single-triggered). Otherwise, the
/// first clock.
///
Expand All @@ -68,6 +73,11 @@ class FiniteStateMachine<StateIdentifier> {
/// bus.
final Logic currentState;

/// A [List] of [Conditional] actions to perform at the beginning of the
/// evaluation of actions for the [FiniteStateMachine]. This is useful for
/// things like setting up default values for signals across all states.
final List<Conditional> setupActions;

/// The next state of the FSM.
///
/// Use [getStateIndex] to map from a [StateIdentifier] to the value on this
Expand All @@ -78,7 +88,7 @@ class FiniteStateMachine<StateIdentifier> {
static int _logBase(num x, num base) => (log(x) / log(base)).ceil();

/// Width of the state.
final int _stateWidth;
final int stateWidth;

/// If `true`, the [reset] signal is asynchronous.
final bool asyncReset;
Expand All @@ -92,17 +102,24 @@ class FiniteStateMachine<StateIdentifier> {
StateIdentifier resetState,
List<State<StateIdentifier>> states, {
bool asyncReset = false,
}) : this.multi([clk], reset, resetState, states, asyncReset: asyncReset);
List<Conditional> setupActions = const [],
}) : this.multi([clk], reset, resetState, states,
asyncReset: asyncReset, setupActions: setupActions);

/// Creates an finite state machine for the specified list of [_states], with
/// an initial state of [resetState] (when [reset] is high) and transitions on
/// positive edges of any of [_clks].
///
/// If [asyncReset] is `true`, the [reset] signal is asynchronous.
FiniteStateMachine.multi(
this._clks, this.reset, this.resetState, this._states,
{this.asyncReset = false})
: _stateWidth = _logBase(_states.length, 2),
this._clks,
this.reset,
this.resetState,
this._states, {
this.asyncReset = false,
List<Conditional> setupActions = const [],
}) : setupActions = List.unmodifiable(setupActions),
stateWidth = _logBase(_states.length, 2),
currentState =
Logic(name: 'currentState', width: _logBase(_states.length, 2)),
nextState =
Expand All @@ -116,26 +133,29 @@ class FiniteStateMachine<StateIdentifier> {
}

Combinational([
...setupActions,
Case(
currentState,
_states
.map((state) => CaseItem(
Const(_stateValueLookup[state], width: _stateWidth), [
...state.actions,
Case(
Const(1),
state.events.entries
.map((entry) => CaseItem(entry.key, [
nextState <
_stateValueLookup[
_stateLookup[entry.value]]
]))
.toList(growable: false),
conditionalType: state.conditionalType,
defaultItem: [
nextState < getStateIndex(state.defaultNextState),
])
]))
Const(_stateValueLookup[state], width: stateWidth)
.named(state.identifier.toString()),
[
...state.actions,
Case(
Const(1),
state.events.entries
.map((entry) => CaseItem(entry.key, [
nextState <
_stateValueLookup[
_stateLookup[entry.value]]
]))
.toList(growable: false),
conditionalType: state.conditionalType,
defaultItem: [
nextState < getStateIndex(state.defaultNextState),
])
]))
.toList(growable: false),
conditionalType: ConditionalType.unique,
defaultItem: [
Expand Down
6 changes: 3 additions & 3 deletions lib/src/utilities/simcompare.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,15 @@ abstract class SimCompare {
}

for (final vector in vectors) {
Simulator.registerAction(timestamp, () {
Simulator.registerAction(timestamp, () async {
for (final signalName in vector.inputValues.keys) {
final value = vector.inputValues[signalName];
(module.tryInput(signalName) ?? getIoInputDriver(signalName))
.put(value);
}

if (enableChecking) {
Simulator.postTick.first.then((value) {
unawaited(Simulator.postTick.first.then((value) {
for (final signalName in vector.expectedOutputValues.keys) {
final value = vector.expectedOutputValues[signalName];
final o =
Expand Down Expand Up @@ -207,7 +207,7 @@ abstract class SimCompare {
(Object err, StackTrace stackTrace) {
Simulator.throwException(err as Exception, stackTrace);
},
);
));
}
});
timestamp += Vector._period;
Expand Down
81 changes: 54 additions & 27 deletions test/fsm_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,13 @@ class TrafficTestModule extends Module {
TrafficPresence.isEastActive(traffic): LightStates.northSlowing,
}, actions: [
northLight < LightColor.green.value,
eastLight < LightColor.red.value,
]),
State(
LightStates.northSlowing,
events: {},
defaultNextState: LightStates.eastFlowing,
actions: [
northLight < LightColor.yellow.value,
eastLight < LightColor.red.value,
],
),
State(
Expand All @@ -139,7 +137,6 @@ class TrafficTestModule extends Module {
TrafficPresence.isNorthActive(traffic): LightStates.eastSlowing,
},
actions: [
northLight < LightColor.red.value,
eastLight < LightColor.green.value,
],
),
Expand All @@ -148,7 +145,6 @@ class TrafficTestModule extends Module {
events: {},
defaultNextState: LightStates.northFlowing,
actions: [
northLight < LightColor.red.value,
eastLight < LightColor.yellow.value,
],
),
Expand All @@ -159,6 +155,11 @@ class TrafficTestModule extends Module {
reset,
LightStates.northFlowing,
states,
setupActions: [
// by default, lights should be red
northLight < LightColor.red.value,
eastLight < LightColor.red.value,
],
);

if (!kIsWeb) {
Expand All @@ -179,23 +180,49 @@ void main() {
});

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

final sv = pipem.generateSynth();
final sv = mod.generateSynth();

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

test('conditional type is used', () async {
final pipem = TestModule(Logic(), Logic(), Logic());
await pipem.build();
final mod = TestModule(Logic(), Logic(), Logic());
await mod.build();

final sv = pipem.generateSynth();
final sv = mod.generateSynth();

expect(sv, contains('priority case'));
});

test('label name included in generated SV', () async {
final mod = TestModule(Logic(), Logic(), Logic());
await mod.build();

final sv = mod.generateSynth();

expect(sv, contains('MyStates_state1 : begin'));
});

test('state value lookup is correct', () async {
final mod = DefaultStateFsmMod(Logic());
await mod.build();

expect(mod._fsm.stateWidth, 2);

expect(mod._fsm.stateIndexLookup.length, MyStates.values.length);
expect(
MyStates.values.every((e) => mod._fsm.stateIndexLookup.containsKey(e)),
isTrue);
for (var i = 0; i < MyStates.values.length; i++) {
final stateEnum = MyStates.values[i];
expect(mod._fsm.getStateIndex(stateEnum), i);
expect(mod._fsm.stateIndexLookup[stateEnum], i);
}
});

group('fsm validation', () {
test('duplicate state identifiers throws exception', () {
expect(
Expand Down Expand Up @@ -231,42 +258,42 @@ void main() {

group('simcompare', () {
test('simple fsm', () async {
final pipem = TestModule(Logic(), Logic(), Logic());
final mod = TestModule(Logic(), Logic(), Logic());

await pipem.build();
await mod.build();

final vectors = [
Vector({'reset': 1, 'a': 0, 'c': 0}, {}),
Vector({'reset': 0}, {'b': 0}),
Vector({}, {'b': 1}),
Vector({'c': 1}, {'b': 0}),
];
await SimCompare.checkFunctionalVector(pipem, vectors);
SimCompare.checkIverilogVector(pipem, vectors);
await SimCompare.checkFunctionalVector(mod, vectors);
SimCompare.checkIverilogVector(mod, vectors);

verifyMermaidStateDiagram(_simpleFSMPath);
});

test('simple fsm async reset', () async {
final pipem =
final mod =
TestModule(Logic(), Logic(), Logic(), testingAsyncReset: true);

await pipem.build();
await mod.build();

final vectors = [
Vector({'reset': 0, 'a': 0, 'c': 0}, {}),
Vector({'reset': 1}, {'b': 0}),
];
await SimCompare.checkFunctionalVector(pipem, vectors);
SimCompare.checkIverilogVector(pipem, vectors);
await SimCompare.checkFunctionalVector(mod, vectors);
SimCompare.checkIverilogVector(mod, vectors);

verifyMermaidStateDiagram(_simpleFSMPath);
});

test('default next state fsm', () async {
final pipem = DefaultStateFsmMod(Logic());
final mod = DefaultStateFsmMod(Logic());

await pipem.build();
await mod.build();

final vectors = [
Vector({'reset': 1}, {}),
Expand All @@ -275,12 +302,12 @@ void main() {
Vector({'reset': 0}, {'b': 4}),
Vector({'reset': 0}, {'b': 4}),
];
await SimCompare.checkFunctionalVector(pipem, vectors);
SimCompare.checkIverilogVector(pipem, vectors);
await SimCompare.checkFunctionalVector(mod, vectors);
SimCompare.checkIverilogVector(mod, vectors);

if (!kIsWeb) {
const fsmPath = '$_tmpDir/default_next_state_fsm.md';
pipem._fsm.generateDiagram(outputPath: fsmPath);
mod._fsm.generateDiagram(outputPath: fsmPath);

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

test('traffic light fsm', () async {
final pipem = TrafficTestModule(Logic(width: 2), Logic());
await pipem.build();
final mod = TrafficTestModule(Logic(width: 2), Logic());
await mod.build();

final vectors = [
Vector({'reset': 1, 'traffic': 00}, {}),
Expand All @@ -315,8 +342,8 @@ void main() {
'eastLight': LightColor.green.value
})
];
await SimCompare.checkFunctionalVector(pipem, vectors);
SimCompare.checkIverilogVector(pipem, vectors);
await SimCompare.checkFunctionalVector(mod, vectors);
SimCompare.checkIverilogVector(mod, vectors);

verifyMermaidStateDiagram(_trafficFSMPath);
});
Expand Down
Loading