// -------------------------------------------------------------------------------------- // Project: MicroManipulatorStepper // License: MIT (see LICENSE file for full description) // All text in here must be included in any redistribution. // Author: M. S. (diffraction limited) // -------------------------------------------------------------------------------------- #include #include "robot.h" #include "hw_config.h" #include "utilities/logging.h" #include "utilities/utilities.h" #include "kinematic_models/kinematic_model_delta3d.h" #include "servo_control/homing_controller.h" #include "servo_control/actuator_calibration.h" #include "robot_joint/robot_joint.h" #include "pico/multicore.h" #include "version.h" #include "robot_tool/pwm_tool.h" constexpr int SPINLOCK_ID_SHARED_DATA = 0; constexpr int SPINLOCK_ID_JOINTS = 1; #include NeoPixelConnect led(PIN_BUILTIN_LED, 1); //*** FUNCTION ************************************************************************** bool startswith(const std::string& str, const std::string& prefix) { return str.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), str.begin()); } //*** CLASS ***************************************************************************** //--- Robot ----------------------------------------------------------------------------- Robot::Robot(float path_segment_time_step) : path_planner(nullptr, path_segment_time_step), motion_controller(&path_planner), servo_loop_frequency_counter(10000), motion_controller_frequency_counter(1000), shared_data(SPINLOCK_ID_SHARED_DATA), joints_spin_lock(spin_lock_instance(SPINLOCK_ID_JOINTS)) { kinematic_model = new KinematicModel_Delta3D(); path_planner.set_kinematic_model(kinematic_model); for(int i=0; iinit(i); joints[i]->load_calibration(); } // add tools robot_tools[0] = new PwmTool(); ((PwmTool*)robot_tools[0])->init(PIN_TOOL1, 8000, 8); robot_tools[1] = new PwmTool(); ((PwmTool*)robot_tools[1])->init(PIN_TOOL2, 8000, 8); // setup timer for updating the motion controller (which evaluates joint space path // segments and produces the current target position for the servo loops) float motion_controller_update_time_us = 500; add_repeating_timer_us(-motion_controller_update_time_us, Robot::update_motion_controller_isr, (void*)this, &motion_controller_update_timer); } void Robot::update_command_parser() { // process serial input if (Serial.available()) { char c = Serial.read(); command_parser.add_input_character(c); // Serial.write(c); } // update command parse which will queue command to the path planner command_parser.update(); // TESTING: //sleep_ms(10); //float pos_error = joints[1]->servo_controller->get_position_error(); //LOG_INFO(">pos_error [µrad]: %f\n", pos_error*1e6); //Serial.printf(">pos_error [µrad]: %f\n", pos_error*1e6);# //LOG_INFO(">pos_x [mm]: %f", joints[1]->position); //update_servo_controllers(0.01f); } /** * Updates the path planner, that chops up kartesian path segments into joint space * path segments using the inverse kinematic model. It then enqueues these joint space path * segments for the motion controller. */ void Robot::update_path_planner() { // check if buffering starts uint64_t time = time_us_64(); if(state == ERobotState::IDLE && path_planner.input_queue_size() > 0) { state = ERobotState::BUFFERING_PATH; path_buffering_start_time = time; } // check if execution starts uint64_t buffering_time = time-path_buffering_start_time; if(state == ERobotState::BUFFERING_PATH && buffering_time > path_buffering_time_us) { state = ERobotState::EXECUTING_PATH; } // execute path if(state == ERobotState::EXECUTING_PATH) { // update planner and generate joint space path segments path_planner.process(true); if(path_planner.all_finished()) state = ERobotState::IDLE; } } /** * Updates the motion controller with a timer interrupt in regular intervals (e.g. 2kHz). * The function evaluates joint space path segments and produces the current * target position for the servo loops. */ bool Robot::update_motion_controller_isr(repeating_timer_t* timer) { float joint_positions[NUM_JOINTS]; float joint_velocities[NUM_JOINTS]; float tool_outputs[NUM_TOOLS]; // get robot pointer Robot* robot = (Robot*)timer->user_data; // get time and delta time uint64_t time_us = time_us_64(); float dt = float(time_us - robot->last_mc_update_time)*1e-6f; robot->last_mc_update_time = time_us; // get current joint position/velocity and tool outputs bool update_ok = robot->motion_controller.update(dt, joint_positions, joint_velocities, tool_outputs); // Attempt to acquire spinlock non-blocking and set new target data for the servo loops if (update_ok && spin_try_lock_unsafe(robot->shared_data.lock)) { for (int i = 0; i < NUM_JOINTS; i++) { robot->shared_data.joint_target_positions[i] = joint_positions[i]; robot->shared_data.joint_target_velocities[i] = joint_velocities[i]; } spin_unlock_unsafe(robot->shared_data.lock); } // update tool outputs if(update_ok) { auto& tools = robot->robot_tools; for(int i=0; iset_value(tool_outputs[i]); } } // update frequency counter robot->motion_controller_frequency_counter.update(dt); return true; // keep repeating } /** * update servo loops, this is called from the second cpu core */ void Robot::update_servo_controllers(float dt) { float one_over_dt = 1.0f/dt; // update axis target position and velocity from shared data spin_lock_unsafe_blocking(shared_data.lock); for(int i=0; i<3; i++) { joints[i]->update_target(shared_data.joint_target_positions[i], shared_data.joint_target_velocities[i]); } spin_unlock_unsafe(shared_data.lock); // update servo loop for each axis spin_lock_unsafe_blocking(joints_spin_lock); for(int i=0; iupdate(dt, one_over_dt); } spin_unlock_unsafe(joints_spin_lock); // update frequency counter servo_loop_frequency_counter.update(dt); } void Robot::enable_servo_control(bool enable) { // LOG_DEBUG(enable ? "Enable servo control" : "Disable servo sontrol"); // update servo loop for each axis spin_lock_unsafe_blocking(joints_spin_lock); for(int i=0; iis_homed && joints[i]->is_calibrated && enable; LOG_DEBUG(en ? "Joint-%i: servo control enabled" : "Joint-%i: servo control disabled", i); joints[i]->servo_controller->set_motor_update_enabled(en); } spin_unlock_unsafe(joints_spin_lock); } void Robot::set_pose(const Pose6DF& pose) { // run inverse kinematic and compute joint positions float joint_positions[NUM_JOINTS]; kinematic_model->inverse(pose, joint_positions); while(true) { // Attempt to acquire spinlock non-blocking and set new target data for the servo loops if (spin_try_lock_unsafe(shared_data.lock)) { for (int i = 0; i < NUM_JOINTS; i++) { shared_data.joint_target_positions[i] = joint_positions[i]; shared_data.joint_target_velocities[i] = 0.0f; // LOG_DEBUG("Joint-%i: set pose -> angle %f", i, joint_positions[i]); } spin_unlock_unsafe(shared_data.lock); break; } } current_pose = pose; } Pose6DF Robot::pose_from_joint_angles() { // read joint positions from encoders float joint_pos[NUM_JOINTS]; spin_lock_unsafe_blocking(joints_spin_lock); for (int i = 0; i < NUM_JOINTS; i++) { joint_pos[i] = joints[i]->servo_controller->read_position(); } spin_unlock_unsafe(joints_spin_lock); // run foreward kinematic model to retrieve pose from joint positions Pose6DF pose; bool ok = kinematic_model->foreward(joint_pos, pose); if(ok == false) LOG_ERROR("Foreward kinematic failed"); return pose; } CommandParser* Robot::get_command_parser() { return &command_parser; } bool Robot::check_all_joints_ready() { bool all_ready = true; for(int i=0; iis_calibrated && joints[i]->is_homed; } return all_ready; } bool Robot::home(uint8_t joint_mask, float retract_angles[NUM_JOINTS]) { HomingController homing_controller[NUM_JOINTS]; LOG_INFO("homing..."); enable_servo_control(false); // prevent servo loop updates from running during homing spin_lock_unsafe_blocking(joints_spin_lock); // initialize homing controllers for(int i=0; i>i)&1) == 0) continue; LOG_DEBUG("start homing axis %i", i); homing_controller[i].start(joints[i]->servo_controller, -HOMING_VELOCITY, 360.0f*DEG_TO_RAD, HOMING_CURRENT, ENCODER_ANGLE_TO_ROTOR_ANGLE, retract_angles[i]); } // run homing controllers bool all_finished = false; while(all_finished == false) { all_finished = true; for(int i=0; i>i)&1) == 0) continue; // uddate homing_controller[i].update(); all_finished &= homing_controller[i].is_finished(); } } // finalize homing controllers bool homing_successful = true; for(int i=0; i>i)&1) == 0) continue; homing_controller[i].finalize(); if(homing_controller[i].is_successful()) { joints[i]->is_homed = true; } else { LOG_ERROR("homing joint %i failed", i); homing_successful = false; } // set joint angles spin_lock_unsafe_blocking(shared_data.lock); shared_data.joint_target_positions[i] = joints[i]->servo_controller->read_position(); spin_unlock_unsafe(shared_data.lock); } // servo updates may continue here spin_unlock_unsafe(joints_spin_lock); // get pose from joint angles set_pose(pose_from_joint_angles()); // enable servo loops if all joints are initialized enable_servo_control(true); // check if all joints are ready all_joints_ready = check_all_joints_ready(); return homing_successful; } bool Robot::calibrate_joint(int joint_idx, bool store_calibration, bool print_measurements) { if(joint_idx<0 || joint_idx >= NUM_JOINTS) return false; RobotJoint* joint = joints[joint_idx]; // prevent servo loop updates from running during homing enable_servo_control(false); spin_lock_unsafe_blocking(joints_spin_lock); bool calibration_ok = joint->calibrate(print_measurements); if(!calibration_ok) { spin_unlock_unsafe(joints_spin_lock); return false; } // joint->servo_controller->move_to_open_loop(0.05f, 1.0); shared_data.joint_target_positions[joint_idx] = 0; // joint->servo_controller->read_position(); if(store_calibration) joint->store_calibration(); // servo updates may continue here spin_unlock_unsafe(joints_spin_lock); // recover pose from joint angles set_pose(pose_from_joint_angles()); // enable servo loops if all joints are initialized enable_servo_control(true); // check if all joints are ready all_joints_ready = check_all_joints_ready(); return true; } //--- G-Code Commands ------------------------------------------------------------------- bool Robot::can_process_command(const GCodeCommand& cmd) { if(cmd.get_command() == "G0" || cmd.get_command() == "G4") { return path_planner.input_queue_full() == false; } return true; } void Robot::send_reply(const char* str) { Serial.write(str); } void Robot::process_command(const GCodeCommand& cmd, std::string& reply) { if(cmd.get_command() == "G0") process_motion_command(cmd, reply); else if(cmd.get_command() == "G1") process_motion_command(cmd, reply); else if(cmd.get_command() == "G4") process_dwell_command(cmd, reply); else if(cmd.get_command() == "G24") process_set_pose_command(cmd, reply); else if(cmd.get_command() == "G28") process_home_command(cmd, reply); else if(startswith(cmd.get_command(), "M")) process_machine_command(cmd, reply); else reply="error: unknown command\n"; } void Robot::process_machine_command(const GCodeCommand& cmd, std::string& reply) { reply = ""; // process tool output command if(cmd.get_command() == "M3") { process_tool_output_command(cmd, reply); return; } // enable motors if(cmd.get_command() == "M17") { // read current pose from HW and set it as current pose set_pose(pose_from_joint_angles()); // enable motors spin_lock_unsafe_blocking(joints_spin_lock); for(int i=0; iservo_controller->set_motor_enabled(true, true); } spin_unlock_unsafe(joints_spin_lock); reply = "ok\n"; return; } // disable motors if(cmd.get_command() == "M18") { spin_lock_unsafe_blocking(joints_spin_lock); for(int i=0; iservo_controller->set_motor_enabled(false, false); spin_unlock_unsafe(joints_spin_lock); reply = "ok\n"; return; } // get current internal position (not using encoders to read physical position) if(cmd.get_command() == "M50") { reply += std::string("X") + std::to_string(current_pose.translation.x); reply += std::string(" Y") + std::to_string(current_pose.translation.y); reply += std::string(" Z") + std::to_string(current_pose.translation.z); reply += "\nok\n"; return; } // get current internal position (not using encoders to read physical position) if(cmd.get_command() == "M51") { for(int i=0; iencoder->get_last_abs_raw_angle(); float angle = joints[i]->encoder->get_last_abs_angle()*Constants::RAD2DEG; reply += std::string("Joint ")+std::to_string(i)+": " + std::to_string(angle) + " deg (raw="+std::to_string(raw_angle)+")\n"; } reply += "ok\n"; return; } // get planner queue size if(cmd.get_command() == "M52") { int s = path_planner.input_queue_size(); reply += std::string("Queue Size: ") + std::to_string(s) + "\n"; reply += "ok\n"; return; } // check if all planned motions are finished executing if(cmd.get_command() == "M53") { bool f = path_planner.all_finished(); reply += f ? "1\n" : "0\n"; reply += "ok\n"; return; } // set servo loop parameters if(cmd.get_command() == "M55") { process_set_servo_parameter_command(cmd, reply); return; } // calibrate joint if(cmd.get_command() == "M56") { process_calibrate_joint_command(cmd, reply); return; } // get info if(cmd.get_command() == "M57") { uint32_t servo_loop_freq = servo_loop_frequency_counter.get(); uint32_t mcontroler_freq = motion_controller_frequency_counter.get(); spin_lock_unsafe_blocking(joints_spin_lock); for(int i=0; iencoder->read_abs_angle()*Constants::RAD2DEG; reply += std::string("Joint ") + std::to_string(i)+":"; reply += std::string(" is_homed=") + std::to_string(joints[i]->is_homed); reply += std::string(" is_calibrated=") + std::to_string(joints[i]->is_calibrated); reply += std::string(" encoder_angle=") + std::to_string(angle) + " deg\n"; } spin_unlock_unsafe(joints_spin_lock); reply += std::string("Servo Loop: ") + std::to_string(servo_loop_freq/1000) + " kHz\n"; reply += std::string("Motion Controler: ") + std::to_string(mcontroler_freq) + " Hz\n"; for(int i=0; iservo_controller->get_enc_to_pos_lut().print_to_log(); return; } // set linear and angular acceleration if(cmd.get_command() == "M204") { if(cmd.has_word('L')) max_acceleration.linear = cmd.get_value('L'); if(cmd.has_word('A')) max_acceleration.angular = cmd.get_value('A'); reply += "ok\n"; return; } } void Robot::process_motion_command(const GCodeCommand& cmd, std::string& reply) { Pose6DF end_pose; #ifndef JOINT_READY_OVERRIDE if(!all_joints_ready) { reply = "error: not all joints calibrated and homed\n"; return; } #endif if(path_planner.input_queue_full()) { reply = "busy\n"; return; } // read feed rate current_feedrate.linear = cmd.get_value('F', current_feedrate.linear); current_feedrate.angular = cmd.get_value('R', current_feedrate.angular); if(cmd.has_word('I')) state = ERobotState::EXECUTING_PATH; // read translation end_pose.translation.x = cmd.get_value('X', current_pose.translation.x); end_pose.translation.y = cmd.get_value('Y', current_pose.translation.y); end_pose.translation.z = cmd.get_value('Z', current_pose.translation.z); // read rotation (all elements must be present) if(cmd.has_word('A') && cmd.has_word('B') && cmd.has_word('C')) { Vec3F rot_vec(cmd.get_value('A'), cmd.get_value('B'), cmd.get_value('C')); end_pose.rotation = QuaternionF::from_rot_vec(rot_vec); } else { end_pose.rotation = current_pose.rotation; } // create path segment CartesianPathSegment path_segment(current_pose, end_pose, current_feedrate, max_acceleration, current_tool_outputs); bool ok = path_planner.add_cartesian_path_segment(path_segment); if(ok) { path_planner.run_look_ahead_planning(); current_pose = end_pose; reply = "ok\n"; } else { reply = "error\n"; } } /** * Immediately sets the current pose without creating path segments or * interpolating from current position. Useful for external realtime controll. */ void Robot::process_set_pose_command(const GCodeCommand& cmd, std::string& reply) { Pose6DF pose; if(!all_joints_ready) { reply = "error: not all joints calibrated and homed\n"; return; } // read translation pose.translation.x = cmd.get_value('X', current_pose.translation.x); pose.translation.y = cmd.get_value('Y', current_pose.translation.y); pose.translation.z = cmd.get_value('Z', current_pose.translation.z); // read rotation (all elements must be present) if(cmd.has_word('A') && cmd.has_word('B') && cmd.has_word('C')) { Vec3F rot_vec(cmd.get_value('A'), cmd.get_value('B'), cmd.get_value('C')); pose.rotation = QuaternionF::from_rot_vec(rot_vec); } else { pose.rotation = current_pose.rotation; } // set the current pose und update target angles for servo loops set_pose(pose); reply = "ok\n"; } void Robot::process_dwell_command(const GCodeCommand& cmd, std::string& reply) { #ifndef JOINT_READY_OVERRIDE if(!all_joints_ready) { reply = "error: not all joints calibrated and homed\n"; return; } #endif if(path_planner.input_queue_full()) { reply = "busy\n"; return; } // get dwell time float dwell_time = 1.0f; if(cmd.has_word('S')) dwell_time = cmd.get_value('S'); // time given in seconds if(cmd.has_word('P')) dwell_time = cmd.get_value('P')*0.001f; // time given in milliseconds // create path segment CartesianPathSegment path_segment(current_pose, current_tool_outputs, dwell_time); bool ok = path_planner.add_cartesian_path_segment(path_segment); if(ok) { path_planner.run_look_ahead_planning(); reply = "ok\n"; } else { reply = "error\n"; } } void Robot::process_set_servo_parameter_command(const GCodeCommand& cmd, std::string& reply) { // example: M55 A150 B50000 C0.2 D100 E F0.0025 bool has_all = cmd.has_word('A') && cmd.has_word('B') && cmd.has_word('C') && cmd.has_word('D') && cmd.has_word('F'); if(has_all == false) reply = "error: not all parameters given (A,B,C,D,F expected)\n"; for(int i=0; iservo_controller->velocity_lowpass.set_time_constant(cmd.get_value('F')); joints[i]->servo_controller->pos_controller.set_parameter(cmd.get_value('A'), cmd.get_value('B'), 0.0f, Constants::PI_F*2.0F, Constants::PI_F*0.5F); joints[i]->servo_controller->velocity_controller.set_parameter(cmd.get_value('C'), cmd.get_value('D'), 0.0f, Constants::PI_F*0.45f, Constants::PI_F*0.45f); } reply = "ok\n"; } void Robot::process_home_command(const GCodeCommand& cmd, std::string& reply) { float retract_angles[NUM_JOINTS] = {-1.0f}; // TODO: check parameter and build joint mask uint8_t joint_mask = 0; for(int i=0; i 1e-3f) retract_angles[i] = retract_angle; } } std::string supported_words = "A,B,C,D,E,F"; if(cmd.contains_unsupported_words(supported_words+",G,M")) { reply = "error: Unsupported parameter found. Only [" + supported_words + "] are supported\n"; return; } if(joint_mask == 0) joint_mask = 255; bool ok = home(joint_mask, retract_angles); reply = ok ? "ok\n" : "error\n"; } void Robot::process_calibrate_joint_command(const GCodeCommand& cmd, std::string& reply) { int idx = cmd.get_value('J', 0); bool store_calibration = cmd.has_word('S'); bool print_measurements = cmd.has_word('P'); bool ok = calibrate_joint(idx, store_calibration, print_measurements); reply = ok ? "ok\n" : "error\n"; } void Robot::process_tool_output_command(const GCodeCommand& cmd, std::string& reply) { // get tool index int tool_index = (int)cmd.get_value('T', 0); if(tool_index < 0 || tool_index >= NUM_TOOLS) { reply = "error: Tool index out of range\n"; return; } // set current tool output value float tool_value = cmd.get_value('S', 0.0f); current_tool_outputs[tool_index] = tool_value; reply = "ok\n"; }