How to use firmware oscilloscope?

I’d like to obtain some high speed (8 kHz) logs of things like currents and torques. I’ve found some references to an onboard oscilloscope function that will log some parameters to a buffer and then dump them to odrive tool, but can’t find any documentation. Can someone direct me to how to do this?

2 Likes

Are you using ODrive 3? You need to dig into the firmware source code to see how it works. If you have specific questions I can answer, I’m successfully using it myself.

Do you mean that this functionality is available if you modify the source code and recompile the firmware, as opposed to something you can configure and use via the python odrivetool?

If you want to change the trigger, you need to recompile the firmware yes. If not, you need to read the source code to understand it at least.

Hi grahameth

I am also attempting to use the oscilloscope but I can’t get it to store the data I want it to. To be honest it won’t even load or make any data files. Do you know how to change the code to use any data type I specify? And do you call it using the odrive tool or start it in some other way?

Hi there,

The firmware must be compiled and flashed to the ODrive in debug mode (with debug symbols).

Check here: https://github.com/odriverobotics/ODrive/blob/58fdd3fdfdcaeff76547870f5e7acdf1673479e8/Firmware/MotorControl/odrive_main.h#L226C30-L226C30, you’ll need to pass a float** (double pointer) to the variable you want to trigger on and capture for the two nullptr respectively. From there, it’ll sample it every loop index (8kHz), and you can use get_val (https://github.com/odriverobotics/ODrive/blob/master/Firmware/MotorControl/oscilloscope.hpp#L14) to return.

Poorly documented on ODrive v3.6, sorry - really just a development tool more than anything else. CAN should be able to get ~1-2kHz variable sampling, that may be easier.

@ODiebODrive
What do you mean with data files? ODrive doesn’t have storage, the oscilloscope data is stored in memory. The firmware uses float to store the data. I personally modified it to store in float16 to save some memory. The RAM in ODrive is quite limited. Yes, you can use odrive tool to use it.
@solomondg You don’t need to compile it in Debug mode, Release works fine :wink:

Hi,

So the function did not create any .csv files but I think it might be because I was using the function incorrectly. Thanks for the help. I tried for a bit to get this working but I have a problem calling the data I want to use. I want to read my shadowcount from the encoder but when I try to call it in the odrive_main.h. I think my general question is how/where do I define a pointer to my data so the oscilloscope actually finds the updated data?
And maybe a second question, what should I use as the trigger? Is it possible to define my own variable here or easier to use something else?

Sorry, I don’t quite understand you. What function are you talking about?
Maybe you should also tell us what you are trying to archieve.

Thanks for the response, the problem I have is to do with the pointers. I am trying to capture the joint angles of one of the axes but I do not know what my pointer should point to in the firmware or where I should define it. I am also trying to get the oscilloscope to trigger upon setting the input mode of the controller to a different mode (autotunning). It seems to me this should be quite doable but whichever way I try it my build fails.

What pointer do you mean? Also, if you get a compiler error, just post it here.

Modifying oscilloscope.cpp in the following way should accomplish what you describe:

void Oscilloscope::update() {
    /*float trigger_data = trigger_src_ ? *trigger_src_ : 0.0f;
    float trigger_threshold = trigger_threshold_;
    float sample_data = data_src_ ? **data_src_ : 0.0f;
    
    if (trigger_data < trigger_threshold) {
        ready_ = true;
    }
    if (ready_ && trigger_data >= trigger_threshold) {
        capturing_ = true;
        ready_ = false;
    }*/

    float sample_data = (float)axes[0]->encoder_.shadow_count_;
    if (axes[0]->controller_.config_.input_mode == Controller::INPUT_MODE_TUNING && !ready_) {
        ready_ = true;
        capturing_ = true;
    }

    if (capturing_) {
        if (pos_ < OSCILLOSCOPE_SIZE) {
            data_[pos_++] = sample_data;
        } else {
            pos_ = 0;
            capturing_ = false;
        }
    }
}

It’s a bit hacky, and I haven’t checked that it compiles, but something like that should work.

Hey,
Thanks for the response. In the meanwhile I actually figured it out! I indeed changed everything to a float, almost a carbon copy of what you did. Sadly I did not get the results I wanted to, though. When I try increasing the osciloscope_size my code stops compiling because of RAM issues. I can only get up to about 8000 values before it stops. I do not know if there is an easy fix for this or if it requires big refactorizations of the ODrive Firmware. I was thinking of using smaller floats but saw there was no direct implementation of something like float_16 on the ODrive.

Yes, the RAM on ODrive is quite limited. If you revert to 0.5.1, you’ll have much more RAM available. Refactorings since that version were quite wasteful with memory sadly.

I also have code for you to store the floats in 16bit to save some space:

#if 1
// this is from: https://stackoverflow.com/questions/1659440/32-bit-to-16-bit-floating-point-conversion
typedef unsigned short ushort;
typedef unsigned int uint;

inline uint as_uint(const float x) {
    union { float f; uint i; } val;
    val.f = x;
    return val.i;
}
inline float as_float(const uint x) {
    union { float f; uint i; } val;
    val.i = x;
    return val.f;
}

inline float half_to_float(ushort x) { // IEEE-754 16-bit floating-point format (without infinity): 1-5-10, exp-15, +-131008.0, +-6.1035156E-5, +-5.9604645E-8, 3.311 digits
    const uint e = (x&0x7C00)>>10; // exponent
    const uint m = (x&0x03FF)<<13; // mantissa
    const uint v = as_uint((float)m)>>23; // evil log2 bit hack to count leading zeros in denormalized format
    return as_float((x&0x8000)<<16 | (e!=0)*((e+112)<<23|m) | ((e==0)&(m!=0))*((v-37)<<23|((m<<(150-v))&0x007FE000))); // sign : normalized : denormalized
}
inline ushort float_to_half(float x) { // IEEE-754 16-bit floating-point format (without infinity): 1-5-10, exp-15, +-131008.0, +-6.1035156E-5, +-5.9604645E-8, 3.311 digits
    const uint b = as_uint(x)+0x00001000; // round-to-nearest-even: add last bit after truncated mantissa
    const uint e = (b&0x7F800000)>>23; // exponent
    const uint m = b&0x007FFFFF; // mantissa; in line below: 0x007FF000 = 0x00800000-0x00001000 = decimal indicator flag - initial rounding
    return (b&0x80000000)>>16 | (e>112)*((((e-112)<<10)&0x7C00)|m>>13) | ((e<113)&(e>101))*((((0x007FF000+m)>>(125-e))+1)>>1) | (e>143)*0x7FFF; // sign : normalized : denormalized : saturate
}

using oscilloscope_type = ushort;
#else
using oscilloscope_type = float;
inline float half_to_float(float x) {
	return x;
}
inline float float_to_half(float x) {
	return x;
}
#endif

Using this on 0.5.1 allows you save about 36000 values, which is about 4.5 seconds.

Wow, thanks.
I am using some custom firmware so I do not know if switching to an old version will do me any favors but I will definitely try this code. Where do you recommend putting this code? Just in the oscilloscope?

Thanks

oscilloscope.h should work.