Skip to content

Conversation

veerwang
Copy link
Contributor

@veerwang veerwang commented Jun 4, 2025

By simultaneously driving the optimization adjustments of the X and Y axes, a performance improvement of approximately 5% was achieved.

Copy link
Collaborator

@ianohara ianohara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a bunch of comments. Some ask for clarification, some for fixes.

At least 2 things we definitely need to do:

  1. Fix the changes to CephlaStage to respect AbstractStage
  2. Decide whether or not this is okay to roll out as is, since it will break all customers until they do a firmware update.

@hongquanli for 2. - how do we want to handle that? If we merge this all customers will break until they do a firmware update (if they update their code to at or post merge of this PR).

// byte[3]: how many micro steps - lower 8 bits

static const int CMD_LENGTH = 8;
static const int CMD_LENGTH = 24;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't figure out why this needs to go up to 24 instead of 10 (we need 2 extra bytes for the MOVE*_XY, but I don't see why we need 11-24).

It's probably okay, but it does mean we send 14 more bytes per command than we absolutely need to. I think that's only ~8 us more per command at 2 MHz, but if we don't need it we might as well not waste the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't thought much about it; I'm just leaving room for future situations. OK, I would change to 10.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command length must be adjusted in multiples of 4 bytes. @ianohara

int target_tolerance;
int pid_tolerance;

int ramp_mode;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we don't initialize this in tmc4361A_init, which means I think we default it to 0. To preserve the original behavior it'd be good to set tmc4361a->ramp_mode = TMC4361_RAMP_SSHAP in the init.

I know we set the ramp mode in our .py implementation, but you generally don't want to assume other pieces of code will do the right thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I agree with you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@veerwang can you implement the suggested change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I would do it.

else
{
focusPosition = focusPosition + (int32_t(uint32_t(buffer[0]) << 24 | uint32_t(buffer[1]) << 16 | uint32_t(buffer[2]) << 8 | uint32_t(buffer[3])) - focuswheel_pos);
focusPosition = focusPosition - (int32_t(uint32_t(buffer[0]) << 24 | uint32_t(buffer[1]) << 16 | uint32_t(buffer[2]) << 8 | uint32_t(buffer[3])) - focuswheel_pos);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't figure out why we switched the sign here. Can you help me understand how this is related to scanning performance, and what the sign switch does?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The knob is designed to increase with clockwise rotation and decrease with counterclockwise rotation. This modification aims to align with conventional intuition.

uint8_t axis = buffer_rx[2];
uint8_t mode = buffer_rx[3];
// map to w axis, in the host w axis index is 5
if (axis == 5)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on the comment above? What code is the host code in this case?

Also, we have AXIS_W defined - can you use that?

And I don't understand why we map AXIS_W to AXIS_THETA here. Can you explain that?

In general - try to avoid using bare numbers like this. Instead, use constants with clear names. This helps make the code more clear, and helps in refactoring later (For instance, if we need to change the axis number for AXIS_W in the future, it's easy to change the AXIS_W define but hard to go and figure out which inline 5 are AXIS_W or some other use of the number 5). Bare numbers are only sortof okay if they are true universal constants (as in, they never change throughout the entire universe).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the python axes match the defines here, so I'm really not sure what the "map to w axis, in host w axis is 5" comment means.

Here are the firmware defines:

static const int AXIS_X = 0;
static const int AXIS_Y = 1;
static const int AXIS_Z = 2;
static const int AXIS_THETA = 3;
static const int AXES_XY = 4;
static const int AXIS_W = 5;

Here are the python defines:

class AXIS:
    X = 0
    Y = 1
    Z = 2
    THETA = 3
    XY = 4
    W = 5

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I would change the code to easy to understand.

axis = 3;

// guard code
if (axis > STAGE_AXES)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it only okay to change the ramp mode for X, Y, and Z? It isn't okay to change them for THETA and W?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For safety reasons, I’ve only tested along the X, Y, and Z axes so far, so I’m keeping it in safe mode for now.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just fails silently, though. If someone tries to use this command with axis > STAGE_AXES, how do they know that they didn't actually set ramp mode on the axis?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we let all other commands fail silently, so I guess we can keep this.

We really need to introduce a mechanism for communicating command failures, though. Failing silently shouldn't be an option!

{
X_commanded_movement_in_progress = false;
mcu_cmd_execution_in_progress = false || Y_commanded_movement_in_progress || Z_commanded_movement_in_progress || W_commanded_movement_in_progress;
us_since_last_check_position = interval_check_position + 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A comment here to explain this might be helpful. Something like "// It's important that we check positions next time around because <explain why ...>, so force the elapsed time to be larger than our interval."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK


self.coordinates_pd = pd.concat([self.coordinates_pd, new_row], ignore_index=True)

def move_to_coordinate(self, coordinate_mm):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's a reason I'm missing here, but this refactor duplicates code. Instead, I probably would do:

def move_to_coordinate(self, coordinate_mm):
    x_mm = coordinate_mm[0]
    y_mm = coordinate_mm[1]
    have_z = len(coordinate_mm) >= 3
    self.stage.move_xy_to(x_mm, y_mm, blocking=not have_z)

    if have_z >= 3:
        self.move_to_z_level(z_mm)
    
    self._sleep(SCAN_STABILIZATION_TIME_MS / 1000)

This:

  1. Does not duplicate the x_mm, y_mm code.
  2. Doesn't have two move_xy_to calls

Also, since SCAN_STABILIZATION_TIME_MS_Y is no longer used for just Y in your code, we should re-name it. This is to follow the "principle of least surprise" - it'd be really surprising to someone in the future to change SCAN_STABILIZATION_TIME_MS_Y and see that all the axis move stabilization times change (not just Y).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I would copy the code.

cmd[5] = payload & 0xFF
self.send_command(cmd)

def move_xy_to_usteps(self, xusteps, yusteps):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we also added a MOVE_XY - we should add that here so users of the Microcontroller know it exists. Or remove it from the firmware.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will temporarily remove MOVE_XY from the firmware.

self._calc_move_timeout(abs_mm - self.get_pos().y_mm, self.get_config().Y_AXIS.MAX_SPEED)
)

def move_xy_to(self, x_abs_mm: float, y_abs_mm: float, blocking: bool = True):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have an AbstractStage interface now. This means the codebase outside of squid/stage can only use methods on the AbstractStage class, and any class that implements AbstractStage must implement all the @abstractmethod on it. As is, this breaks the PriorStage and the AbstractStage interface.

To do this change properly, we should:

  1. Add an @abstractmethod called move_xy_to to AbstractStage.
  2. Implement move_xy_to on all the stage implementations in squid/stage (right now, just the cephla and prior stages).

Test, or find someone that can help you test, the new code on all the stages we support.

We should not merge this before doing all of the above since it will break some customers!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I would add an @AbstractMethod called move_xy_to to AbstractStage. Would you please ask somebody do the implementation of move_xy_to in to prior_stages? @hongquanli

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or I just add an empty function in prior_stages?

@veerwang veerwang requested a review from ianohara July 16, 2025 00:40
@veerwang veerwang requested a review from hongquanli July 29, 2025 00:38
Copy link
Collaborator

@ianohara ianohara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other than adding the PriorStage definition, this looks good (assuming it has been tested on all the Cephla stage hardware it will need to run on).

@hongquanli we still need to decide what we want to do in terms of managing firmware updates. Once this merges, users will need to do a firmware update if they update their repos.

axis = 3;

// guard code
if (axis > STAGE_AXES)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just fails silently, though. If someone tries to use this command with axis > STAGE_AXES, how do they know that they didn't actually set ramp mode on the axis?

threading.Thread(target=self.wait_for_stop, daemon=True).start()

def move_xy_to(self, x_abs_mm: float, y_abs_mm: float, blocking: bool = True):
pass
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I think you can just make this:

def move_xy_to(self, x_abs_mm: float, y_abs_mm: float, blocking: bool = True):
    self.move_x_to(x_abs_mm, blocking)
    self.move_y_to(y_abs_mm, blocking)

axis = 3;

// guard code
if (axis > STAGE_AXES)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we let all other commands fail silently, so I guess we can keep this.

We really need to introduce a mechanism for communicating command failures, though. Failing silently shouldn't be an option!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants