Stepper motor analyzer

My goal is to build a low cost stand alone public domain reference design of a stepper motor analyzer that can be used by makers to measure performance and diagnose issues with stepper motors in 3D printers and such.

The hardware is very simple, a STM32F401CE blackpill connected to the analog output of two ACS70331 current sensors that measure the current of the stepper coils and a 3.5" 480x320 touch screen for the user interface. Most fo the difficult part already works and now I make final touches before designing the PCB.

LVGL related points:

  • I started with a STM32F103 CPU but pretty much realized that is doesn’t have sufficient resourced to use LVGL with my display so upgraded to STM32F401CE (faster and more ROM/RAM).
  • I started driving the ILI9488 480x320 display with SPI because of it’s simplify but got a terrible screen refresh time, especially when adding a new point to a large shifting chart, so switched to 16 bit parallel data path to the TFT.
  • I am running LVGL in 8 bits color depth which works well, but the ILI9488 doesn’t support 8 bit colors in parallel mode so I am using a lookup table uint8_t -> uint16_t which maps the 8 bit colors from the LVGL to 16 bit color to send to the TFT.
  • Because of pins restrictions on the MCU I couldn’t allocate a whole 16 bit port to the TFT parallel output and had to spread the bits across 2 ports, A and B. To be able to update just those bits fast, I am using two lookup tables uint8_t -> uint32_t that takes a 8 bit target value and return the 32bit value to write to the BSSR register to set this value without affecting other bits. These two tables and the color table mentioned above are using in my LVGL TFT driver in an optimized loop.
  • Because my 16 bits to the TFT are spread across ports, I couldn’t find a reasonable way to update the TFT with DMA. Therefore I am using a single LVGL buffer.
  • The fonts that came with LVGL are not monospaced and as a results, numeric fields that update frequently didn’t look good, jumping with each value change. I ended up editing the fonts to have just 0-9 having fixed size and it looks much better now.
  • So far I didn’t need to do any special LVGL RAM optimizations, such as creating screens and widgets dynamically only when I need them. It makes things simpler.
  • I am using platformio and let it manage the LVGL library and this works very well. I specified a specific LVGL version to avoid surprises.
  • I am using the LVGL simulator to experiments with new widgets but once I move it to the actual hardware I drop the simulator and tweak the real thing.
  • Still need to figure out how to capture screen shots for publication. Got some ideas from a thread here. Builtin support from LVGL would be useful, especially for projects that can’t buffer an entire screen.
  • Having access to the LVGL source code is very useful and replaces missing details in the documentation For example the documentation doesn’t mention if styles that are passed to macros are copied or the user’s point is retained so I can check the source code.

This osciloscope like screen captures current signals from the stepper motor coils. It uses a trigger detection that position the signals at a consistent position on the chart. Stepper signals look healthy here.

This screen shows an histogram of coil current as a function of stepper speed. In this example, the current drops at about 1K steps/sec because of high inductance and insufficient supply voltage.

This screen shows the stepper coils currents at higher speed. Looks non healthy. Another indication of high inductance and low supply voltage.

This one is a live gauge that shows the momentary stepper speed. The LVGL works very well and I can easily update > 10fps.

This the home screen with various information about the data collected. Stepper coil current, quadrature errors detected, accumulated steps, and so on.

4 Likes

Looks great and very informative!

Thanks for sharing!

Have you already uploaded it somewhere?

I perform the development against github but keeping private because the code needs major cleanup. I hope to open it in a month or two.

There is a proof of concept that I developed earlier using a Teensy 4 (it’s a beast), a 3.5 Nextion screen and Arduino IDE. https://github.com/zapta/stepper_analyzer.

The new one will be more robust, less expensive, easier to develop, and easier to upgrade and LVGL has a big part in it. :wink:

1 Like

Cool, thank you. :slight_smile:

In my previous post I described 16 bit parallel color path using three table lookups, one that maps the LVGL color8 to color16 and two additional that determines the STM32 BSSR register masks that needed to set the the pins on the two ports I am using.

On a second thought I realized that this can be reduce to two only lookup tables, or more accurately, one table lookup per port that is participates in outputing the TFT data.

The tight loop of updating the TFT now looks like below, two table lookups sets up the 16 data bits as well as resetting the WR output then WR output is brought high to complete the color trasnfer.

uint8_t c8 = next color 8
porta->BSSR = bssr_color_table_a[c8];
portb->BSSR = bssr_color_table_b[c8];
set WR output high.

Another advantage of this approach is that it allows an arbitrary assignment of D15-D0 to MCU pins which made the PCB routing much easier. I first routed the lines and then assigned them to MCU pins.

If anybody is interested, the Java code that generated my tables is in the link below. You will need to adjust the pin table to your pin assignment.

1 Like

Have you already uploaded it somewhere?

It is now published here https://github.com/zapta/simple_stepper_motor_analyzer

Schematic here https://github.com/zapta/simple_stepper_motor_analyzer/blob/master/kicad/stepper_analyzer-sch.pdf

The widgets I use were buttons, charts (bars and graphs), labels and a checkbox. Each screen is in its own screen object and on/off loading them as needed. Most of the layouts with explicit x/y positions.

Overall LVGL performs very well. I may over complicated the event mechanism. I wanted to have my own event enum and defined for each event type a LCGL event callback that calls the enteral event handle with that hard coded event. Would be nice to be able to register with each callback also user data, that is user data at the callback level, not the widget level. I don’t know if it’s possible.

Multiple event callbacks will be supported in 8.0; I think user data per callback will come as part of that.

1 Like

I didn’t think of that but seems really reasonable.

I might be wrong, but I’m pretty sure it would be necessary for MicroPython, since the binding script uses user_data to attach a MicroPython function to the callback.

True, maybe it could a different thing. user_event_data or so?

The motivation:
In v8 I added “draw hook events” that are triggered before and after a specific part of the widget is drawn to allow customization. E.g. called on every cell of the table and button matrix to let the user modify the draw descriptors and/or add custom content (such as manually draw a check box on a table cell).

In lv_components and imagined a lot of user-contributed custom drawers. E.g.: one to make every 2nd row of a table slightly darker, or add a checkbox to the first column, or make a cell red if its value is too high etc. Users can add multiple custom drawers as they are only events.

These events might require custom settings related to the specific object. My idea so far was to make the “extensions” manage a linked list with obj-data pairs and search for obj when data is needed during drawing. This data can be easily removed as the events see the LV_EVENT_DELETE too. But if every event could have its own void* data things would be much simpler.

How about defining an lv_event_user_data struct in lv config and let users decide what they need to have there?

LVGL then can store and pass those struct values as a strong type (rather than void* and such), including to the user’s callbacks which will then use the individual fields.

One more somewhat related idea for V8, when we pass data to LVGL sometimes it allocates memory and makes a copy (e.g. label text) and sometimes it keeps a reference (e.g. style pointers, label static text). In the cases that it makes a copy, how about skipping it automatically if the target of the pointer is known to be immutable? E.g. user specifies in lv config the range of local flash read only memory and LVGL uses that information to avoid copying it to memory.

I’m thinking about simplifying the user data types to have only a global user data type instead of dedicated animation/group/obj/event/filesystem etc user data. It’s because I assume in 99% of the cases user_data is not used, and in the 99% of that 1% cases it’s fine with void*.

Anyway, I’m still considering the effects of such a change. E.g. registering an event would have an extra attribute. E.g. lv_obj_add_event_cb(obj, my_event, my_data)

Interesting idea. Ideally, it’d mean _static functions are not required as LVGL would know automatically if the data is immutable. However, we can’t expect that beginners will figure out this range.

As they can be more ranges (external flash or internal flash) a custom function to tell if the address is immutable would be more flexible.

I think that’s fine; it would also make C++ event functions easier to implement. The legacy function is a different name (lv_obj_set_event_cb) anyways, so backwards compatibility can be provided at compile time.

Okay, I’ve added it. See here.

A practical example here

I think this makes the event mechanism more intuitive. Thanks.

How about making the user data a customizable type? E.g. in lvcong have the default

struct UserData {
void* ptr;
};

and users can customize it to

struct UserData {
// none
};

or

struct UserData {
MyEnum my_enum;
int screen_id;
const char* debug_info;
}

And lv_event_get_user_data() will return a UserData*.

Just a thought.

I was thinking about it but it’d make it very difficult (theoretically impossible) to create reusable components and examples that use the event’s user data. E.g. the example (like the one I linked above) assumes that the user data type is void* but another example could use my_special_type.

Thanks @kisvegabor.

Let’s say that lvconf will have a default UserData definition which the user can change. E.g.

struct UserData {
void* ptr;
};

Will providing examples based on the default UserData be good enough? I presume that users are aware that if they change that struct, they also need to change the way they access it.

Can you mention some real-life example where void* can’t work well?

I could imagine these 3 approaches to work with void*

  1. Use pointer to global/static data
static int id1 = 13;
lv_obj_add_event_cb(obj, func, &id1);

static int id2 = 45;
lv_obj_add_event_cb(obj, func, &id2);
  1. Use union for small types
typedef union {
  uint8_t num;
 void * ptr;
} my_type;

my_type t = {.num = 7};
lv_obj_add_event_cb(obj, func, (my_type) t);
  1. malloc data with any type
my_type * t = malloc(sizeof(my_type));
t->x = ...;
lv_obj_add_event_cb(obj, func, t); //Free `t` in `LV_EVENT_DELETED`

What you can’t do is storing a larger struct as user data (not a pointer). But it’s not recommended anyway because it will consume memory for every callback.

1 Like

@kisvegabor, I think that there is a fourth option, which possibly how I would use void* in my case, that is casting id1 an id2 in your example into void*.

I can’t say that void* will not ‘work well’ but in general I prefer to use static typing which void* is not.

True!

Let’s go with void* for now and let’s see if we got requests to change it.