odriverobotics / ODrive

High performance motor control
https://odriverobotics.com
MIT License
2.89k stars 1.5k forks source link

Two instances of UB and a couple of questions about implementation #745

Open felix91gr opened 4 months ago

felix91gr commented 4 months ago

Hi yall, I'm working on porting https://github.com/odriverobotics/ODrive/blob/master/Firmware/MotorControl/sensorless_estimator.cpp to a proof of concept project of a friend of mine.

I have 2 instances of UB to report in that implementation, and something to ask about the alpha-beta (Clarke's) transform's implementation that I haven't been able to fully understand.

First, the UB.

Undefined Behavior

Default values for current_meas leave some of it uninitialized

On the section between lines 39 and 44, we check if the motor is armed and if it isn't, we set the current measurement values to 0. Or that's the intent, anyway:

https://github.com/odriverobotics/ODrive/blob/6c3cefdeacce600ba4524f4adc5fd634c7bebd18/Firmware/MotorControl/sensorless_estimator.cpp#L39-L44

Because as far as I can tell, the struct used to store the current measurement has 3 fields, not 2:

https://github.com/odriverobotics/ODrive/blob/6c3cefdeacce600ba4524f4adc5fd634c7bebd18/Firmware/odrive-interface.yaml#L12

And this means that in the best case, on line 43 we're only setting two of them:

https://github.com/odriverobotics/ODrive/blob/6c3cefdeacce600ba4524f4adc5fd634c7bebd18/Firmware/MotorControl/sensorless_estimator.cpp#L43

Working with uninitialized memory is Undefined Behavior, and even if it works currently, any future or alternative version of the compiler is allowed to break it.

As mitigation against this undefined behavior, I'd suggest current_meas to be initialized to {0.0f, 0.0f, 0.0f} in line 43 instead.

Eta (η) estimator is not initialized on creation, which makes it fragile against future changes

The vector function eta (η) is not initialized on creation. It is initialized afterwards, in the loop between lines 59 and 69:

https://github.com/odriverobotics/ODrive/blob/6c3cefdeacce600ba4524f4adc5fd634c7bebd18/Firmware/MotorControl/sensorless_estimator.cpp#L58-L69

This works fine for now, but this is the only instance where Eta is written to before it is read in line 73:

https://github.com/odriverobotics/ODrive/blob/6c3cefdeacce600ba4524f4adc5fd634c7bebd18/Firmware/MotorControl/sensorless_estimator.cpp#L73

This means that Eta's initialization depends on the loop that comes beforehand. Any changes that make reading Eta possible earlier could make it so the driver reads an uninitialized value. Doing so would not only make the values unpredictable; Eta is a cosine-sine pair, which means it has a very well-defined domain in which it is valid. The rest of the program assumes (of course; why wouldn't it?) that Eta is a valid element of its domain. Reading an uninitialized Eta would throw all sorts of invariants away, and would make the driver's behavior completely unpredictable.

As mitigation against this undefined behavior, I'd suggest Eta to be initialized to some value that is valid inside its domain. A [1, 0] value would suffice, or anything else that is a valid sin-cos pair.

The alpha-beta (Clarke's) transform implementation

Between the lines 52 and 55, we transform the current measurements using the alpha-beta transform, in order to get a value inside the fixed alpha-beta-zero reference frame:

https://github.com/odriverobotics/ODrive/blob/6c3cefdeacce600ba4524f4adc5fd634c7bebd18/Firmware/MotorControl/sensorless_estimator.cpp#L52-L55

This version of the transform however, seems slightly strange to me. In the wikipedia section for the Simplified Alpha-Beta Transformation, which is the version of the Clarke Transform that leaves us in the alpha-beta-zero reference frame, it says that the simplified transformation matrix looks like this:

image

The current implementation however, does none of the above. Instead, it uses a matrix more like this one:

image

I think the math checks out, but it still surprised me for a day or two. ¿Is this the intended matrix? It looks like it was optimized to be this way. I'm just asking to be completely sure, since automatic control math is kind of at the border of my expertise.

samuelsadok commented 3 months ago

Default values for current_meas

Yes that's true. In current-gen ODrive firmware (see note) this part is slightly refactored, so it's not an issue anymore, but you might wanna change it in your port or feel free make a PR with this change.

Eta is not initialized

The for loop can be seen as the initialization of eta. If we wanna be pedantic, we could put the content of the for-loop into a function and then have the declaration and initialization on the same line, but that would reduce readability in other ways. A default init value would be misleading IMO cause it's never used and distracts from the fact that the for-loop does the initialization.

Clarke's transform

Yup the transforms on Wikipedia and in ODrive firmware are the same: