diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h index c45c75a39d96..1c78602d99e3 100644 --- a/Marlin/Configuration.h +++ b/Marlin/Configuration.h @@ -124,6 +124,11 @@ //#define RS485_BUS_BUFFER_SIZE 128 #endif +// Enable CAN bus support and protocol +//#define CAN_HOST +//#define CAN_TOOLHEAD +//#define CAN_DEBUG + // Enable the Bluetooth serial interface on AT90USB devices //#define BLUETOOTH diff --git a/Marlin/src/HAL/STM32/CAN_host.cpp b/Marlin/src/HAL/STM32/CAN_host.cpp new file mode 100644 index 000000000000..3987db7243dd --- /dev/null +++ b/Marlin/src/HAL/STM32/CAN_host.cpp @@ -0,0 +1,852 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/** + * Contributor Notes: + * NOTE 1: For MKS Monster 8 V1/V2 on Arduino use: Board "Generic STM32F4 series", Board part number "Generic F407VETx" + * NOTE 2: Requires `HAL_CAN_MODULE_ENABLED`, e.g., with `-DHAL_CAN_MODULE_ENABLED` + * For Arduino IDE use "hal_conf_extra.h" with `#define HAL_CAN_MODULE_ENABLED` + * NOTE 3: To accept all CAN messages, enable 1 filter (FilterBank = 0) in "FilterMode = CAN_FILTERMODE_IDMASK", mask and ID = 0 (0=don't care) + * NOTE 4: Serial communication in ISR causes issues! Hangs etc. so avoid! + * NOTE 5: A FIFO storage cell is called a "Mailbox" in STM32F4xx, FIFO0 and FIFO1 can (onlyd)hold 3 CAN messages each. + * NOTE 6: The filter ID/mask numbers (LOW/HIGH) do not directly relate to the message ID numbers (See Figure 342 in RM0090) + */ + +#include "../../inc/MarlinConfigPre.h" + +#if ALL(CAN_HOST, STM32F4xx) + +#include "../platforms.h" +#include "../../gcode/parser.h" +#include "../../module/temperature.h" +#include "../../module/motion.h" // For current_position variable +#include "../../module/planner.h" // For steps/mm parameters variables +#include "../../feature/tmc_util.h" +#include "../../module/endstops.h" +#include "../../feature/controllerfan.h" // For controllerFan settings +#include "../../libs/numtostr.h" // For float to string conversion + +#include "../shared/CAN.h" + +// Interrupt handlers controlled by the CAN_IER register +extern "C" void CAN1_RX0_IRQHandler(void); // CAN1 FIFO0 interrupt handler (new message, full, overrun) +extern "C" void CAN1_RX1_IRQHandler(void); // CAN1 FIFO1 interrupt handler (new message, full, overrun) +extern "C" void CAN1_SCE_IRQHandler(void); // CAN1 status change interrupt handler + +extern "C" void CAN2_RX0_IRQHandler(void); // CAN2 FIFO0 interrupt handler (new message, full, overrun) +extern "C" void CAN2_RX1_IRQHandler(void); // CAN2 FIFO1 interrupt handler (new message, full, overrun) +extern "C" void CAN2_SCE_IRQHandler(void); // CAN2 status change error interrupt handler (See CAN_ESR/CAN_MSR registers) + +extern "C" void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan); // CAN FIFO0 new message callback +extern "C" void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan); // CAN FIFO1 new message callback +extern "C" void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan); // CAN error interrupt callback + +#ifndef CAN_BAUDRATE + #define CAN_BAUDRATE 1000000LU +#endif + +#ifndef CAN_RD_PIN + #define CAN_RD_PIN PB8 +#endif +#ifndef CAN_TD_PIN + #define CAN_TD_PIN PB9 +#endif + +#if (CAN_BAUDRATE != 1000000) && (CAN_BAUDRATE != 500000) && (CAN_BAUDRATE != 250000) && (CAN_BAUDRATE != 125000) + #error ERROR: Select a valid CAN_BAUDRATE: 1000000, 500000, 250000 or 125000 baud +#endif + +#define CAN_HOST_FIFO_DEPTH 3 // RX and TX FIFO0 and FIFO1 depth + +CAN_HandleTypeDef hCAN1 = { 0 }; // The global CAN handle +CAN_TxHeaderTypeDef TxHeader = { 0 }; // Header to send a CAN message +volatile uint32_t CAN_io_state = 0; // Virtual IO state variable +volatile bool CAN_toolhead_error = 0; // Register if an error was reported by the toolhead +volatile bool CAN_toolhead_setup_request = false; // Signals the toolhead requested setup information +volatile bool CAN_time_sync_request = false; // Signals the toolhead requested a time sync +volatile uint32_t time_sync_request_time = 0; // Record the time the time sync request was received + +volatile uint32_t HAL_CAN_error_code = 0; // Store the HAL CAN error code +volatile uint32_t CAN_host_error_code = 0; // Store the CAN host error code + +volatile bool first_E0_error = true; // First CAN bus error, show warning only once +volatile bool string_message_complete = false; // Signals a complete string message was received +uint32_t CAN_next_temp_report_time = 12000; // Track when the next toolhead temperature report should have arrived +uint32_t CAN_next_error_message_time = 0; // Track when to display the next repeat of an error message +volatile bool CAN_host_FIFO_toggle_bit = false; // FIFO toggle flag for receiver FIFO filtering +SString string_message; // CAN string message buffer for incoming messages + +uint32_t CAN_host_get_iostate() { + return CAN_io_state; +} + +uint32_t CAN_set_extended_id(int gcode_type, int gcode_no, int parameter1, int parameter2, int count) { + + CAN_host_FIFO_toggle_bit = !CAN_host_FIFO_toggle_bit; // FIFO toggle bit + + return (CAN_host_FIFO_toggle_bit ? EXTID_FIFO_TOGGLE_BIT : 0) | // FIFO toggle bit + (count << CAN_ID_PARAMETER_COUNT_BIT_POS) | // Parameter count + (gcode_type << CAN_ID_GCODE_TYPE_BIT_POS) | // G/M/T/D-code + ((gcode_no & CAN_ID_GCODE_NUMBER_MASK) << CAN_ID_GCODE_NUMBER_BIT_POS) | // Gcode number + ((parameter1 & CAN_ID_PARAMETER_LETTER_MASK) << CAN_ID_PARAMETER1_BIT_POS) | // First parameter + ((parameter2 & CAN_ID_PARAMETER_LETTER_MASK) << CAN_ID_PARAMETER2_BIT_POS); // Second parameter + +} + +// Send time sync timestamp of arrival and response time +void CAN_host_send_timestamp() { // Request receive timestamp + request response timestamp + + uint32_t TxMailbox; // Stores which Mailbox (0-2) was used to store the sent message + + TxHeader.IDE = CAN_ID_EXT; + TxHeader.DLC = 8; // Send sync time t1(receive time, uint32_t) and t2(response time, uint32_t) + TxHeader.ExtId = CAN_set_extended_id(CAN_ID_GCODE_TYPE_M, CAN_HOST_GCODE_TIME_SYNC_NO, 1, 2, 2); + + uint8_t CAN_tx_buffer[8]; // 8 bytes CAN data TX buffer + uint32_t * uint32p = (uint32_t *)CAN_tx_buffer; // Point to TX buffer + *uint32p++ = time_sync_request_time; + + uint32_t deadline = millis() + CAN_HOST_MAX_WAIT_TIME; + while ((HAL_CAN_GetTxMailboxesFreeLevel(&hCAN1) < CAN_HOST_FIFO_DEPTH) && PENDING(millis(), deadline)) { /* BLOCKING! Wait for empty TX buffer */ } + + if (HAL_CAN_GetTxMailboxesFreeLevel(&hCAN1)) { + *uint32p = micros(); // Only record the response time at the last possible moment + HAL_CAN_AddTxMessage(&hCAN1, &TxHeader, CAN_tx_buffer, &TxMailbox); // Queue CAN message + } + else + CAN_host_error_code |= CAN_ERROR_HOST_TX_MSG_DROPPED; + +} + +// Send specified Gcode with max 2 parameters and 2 values via CAN bus +HAL_StatusTypeDef CAN_host_send_gcode_2params(uint32_t Gcode_type, uint32_t Gcode_no, uint32_t parameter1, float value1, uint32_t parameter2, float value2) { + + HAL_StatusTypeDef status = HAL_OK; + + switch (Gcode_type) { + + case 'D': + Gcode_type = CAN_ID_GCODE_TYPE_D; + return HAL_ERROR; + break; + + case 'G': + Gcode_type = CAN_ID_GCODE_TYPE_G; + return HAL_ERROR; + break; + + case 'M': + Gcode_type = CAN_ID_GCODE_TYPE_M; + + #if ENABLED(CAN_DEBUG) + SERIAL_ECHOPGM("; MSG to toolhead: \"M", Gcode_no); + if (parameter1) { + SERIAL_CHAR(' ', parameter1); + if (value1 == int(value1)) + SERIAL_ECHO(int(value1)); // Integer value + else + SERIAL_ECHO(p_float_t(value1, 4)); // Float with 4 digits + } + + if (parameter2) { + SERIAL_CHAR(' ', parameter2); + if (value2 == int(value2)) + SERIAL_ECHO(int(value2)); // Integer value + else + SERIAL_ECHO(p_float_t(value2, 4)); // Float with 4 digits + } + SERIAL_ECHOLN("\""); + #endif + + break; + + case 'T': Gcode_type = CAN_ID_GCODE_TYPE_T; + return HAL_ERROR; + break; + + default: + return HAL_ERROR; // Unknown Gcode type + } + + if (parameter1 > 31) + parameter1 -= 64; // Format 'A' = 1, 'B' = 2, etc. + + if (parameter2 > 31) + parameter2 -= 64; // Format 'A' = 1, 'B' = 2, etc. + + TxHeader.IDE = CAN_ID_EXT; + TxHeader.DLC = 4 * (!!parameter1 + !!parameter2); // Amount of bytes to send (4 or 8) + + TxHeader.ExtId = CAN_set_extended_id(Gcode_type, Gcode_no, parameter1, parameter2, TxHeader.DLC >> 2); + + uint8_t CAN_tx_buffer[8]; // 8 bytes CAN data TX buffer + float * fp = (float *)CAN_tx_buffer; // Point to TX buffer + *fp++ = value1; + *fp = value2; + + uint32_t TxMailbox; // Stores which Mailbox (0-2) was used to store the sent message + const uint32_t deadline = millis() + CAN_HOST_MAX_WAIT_TIME; + while ((HAL_CAN_GetTxMailboxesFreeLevel(&hCAN1) == 0) && PENDING(millis(), deadline)) { /* BLOCKING! Wait for empty TX buffer */ } + + if (HAL_CAN_GetTxMailboxesFreeLevel(&hCAN1)) + status = HAL_CAN_AddTxMessage(&hCAN1, &TxHeader, CAN_tx_buffer, &TxMailbox); // Queue CAN message + else + CAN_host_error_code |= CAN_ERROR_HOST_TX_MSG_DROPPED; + + return status; +} + +void CAN_host_send_setup(bool changeStatus) { // Send setup to toolhead + + // NOTE: Sending many command too fast will cause a Marlin command buffer overrun at the toolhead, add delays if needed + CAN_toolhead_setup_request = false; + + SERIAL_ECHOLNPGM(">>> CAN: Sending configuration to toolhead..."); + + #if ENABLED(MPCTEMP) + // M306 MPC settings (managed by host) + MPC_t &mpc = thermalManager.temp_hotend[0].mpc; + + CAN_host_send_gcode_2params('M', 306, 'A', mpc.ambient_xfer_coeff_fan0, 'C', mpc.block_heat_capacity); // M306 R A + CAN_host_send_gcode_2params('M', 306, 'F', mpc.fanCoefficient(), 'H', mpc.filament_heat_capacity_permm); // M306 F H + CAN_host_send_gcode_2params('M', 306, 'P', mpc.heater_power, 'R', mpc.sensor_responsiveness); // M306 P C + + #endif + + //CAN_host_send_gcode_2params('M', 150, 0, 0, 0, 0); // M150, SWITCH NEOPIXEL OFF + + /* + extern Planner planner; // M92 Steps per mm + CAN_host_send_gcode_2params('M', 92, 'X', planner.settings.axis_steps_per_mm[X_AXIS], 'Y', planner.settings.axis_steps_per_mm[Y_AXIS]); + CAN_host_send_gcode_2params('M', 92, 'Z', planner.settings.axis_steps_per_mm[Z_AXIS], 'E', planner.settings.axis_steps_per_mm[E_AXIS]); + + // M200 Set filament diameter + CAN_host_send_gcode_2params('M', 200, 'S', parser.volumetric_enabled, 'D', LINEAR_UNIT(planner.filament_size[0])); + #if ENABLED(VOLUMETRIC_EXTRUDER_LIMIT) + CAN_host_send_gcode_2params('M', 200, 'L', LINEAR_UNIT(planner.volumetric_extruder_limit[0]) + #endif + + // M201 Max acceleration + CAN_host_send_gcode_2params('M', 201, 'X', planner.settings.max_acceleration_mm_per_s2[X_AXIS], 'Y', planner.settings.max_acceleration_mm_per_s2[Y_AXIS]); + CAN_host_send_gcode_2params('M', 201, 'Z', planner.settings.max_acceleration_mm_per_s2[Z_AXIS], 'E', planner.settings.max_acceleration_mm_per_s2[E_AXIS]); + + // M203 Max feedrate + CAN_host_send_gcode_2params('M', 203, 'X', planner.settings.max_feedrate_mm_s[X_AXIS], 'Y', planner.settings.max_feedrate_mm_s[Y_AXIS]); + CAN_host_send_gcode_2params('M', 203, 'Z', planner.settings.max_feedrate_mm_s[Z_AXIS], 'E', planner.settings.max_feedrate_mm_s[E_AXIS]); + + // M204 Accelerations in units/sec^2, ENABLED BECAUSE IT INFORMS THE TOOLHEAD THE CONFIGURATION WAS SENT + CAN_host_send_gcode_2params('M', 204, 'P', planner.settings.acceleration, 'R', planner.settings.retract_acceleration); + CAN_host_send_gcode_2params('M', 204, 'T', planner.settings.travel_acceleration, 0, 0); + + // M205 + #if ENABLED(CLASSIC_JERK) + CAN_host_send_gcode_2params('M', 205,'S')) planner.settings.min_feedrate_mm_s, 'T')) planner.settings.min_travel_feedrate_mm_s); + CAN_host_send_gcode_2params('M', 205, M205_MIN_SEG_TIME_PARAM, planner.settings.min_segment_time_us, 'J', planner.junction_deviation_mm); + CAN_host_send_gcode_2params('M', 205, 'X', LINEAR_UNIT(planner.max_jerk.x), 'Y', LINEAR_UNIT(planner.max_jerk.y)); + CAN_host_send_gcode_2params('M', 205, 'Z', LINEAR_UNIT(planner.max_jerk.z), 'E', LINEAR_UNIT(planner.max_jerk.e)); + CAN_host_send_gcode_2params('M', 205, 'J', LINEAR_UNIT(planner.junction_deviation_mm), 0, 0); + #endif + + // M206 Home offset + #if DISABLED(NO_HOME_OFFSETS) + CAN_host_send_gcode_2params('M', 206, 'X', LINEAR_UNIT(home_offset.x), 'Y', LINEAR_UNIT(home_offset.y)); + CAN_host_send_gcode_2params('M', 206, 'Z', LINEAR_UNIT(home_offset.z), 0, 0); + #endif + + // M207 Set Firmware Retraction + // M208 - Firmware Recover + // M209 - Set Auto Retract + + // M220 Speed/feedrate + CAN_host_send_gcode_2params('M', 220, 'S', feedrate_percentage, 0, 0); + + // M221 Flow percentage + CAN_host_send_gcode_2params('M', 221, 'T', 0, 'S', planner.flow_percentage[0]); + // CAN_host_send_gcode_2params('M', 221, 'T', 1, 'S', planner.flow_percentage[1]); // For 2nd extruder + + // M302 Cold extrude settings + #if ENABLED(PREVENT_COLD_EXTRUSION) + CAN_host_send_gcode_2params('M', 302, 'P', '0' + thermalManager.allow_cold_extrude, 'S', thermalManager.extrude_min_temp); // P0 enable cold extrusion checking, P1 = disabled, S=Minimum temperature + #endif + + // M569 TMC Driver StealthChop/SpreadCycle + CAN_host_send_gcode_2params('M', 569, 'S', stepperE0.get_stored_stealthChop(), 'E', 0); // M569 S[0/1] E + + // M592 Nonlinear Extrusion Control + + // M916 TMC Motor current + CAN_host_send_gcode_2params('M', 906, 'E', stepperE0.getMilliamps(), 0, 0); + + // M919 TMC Chopper timing for E only + CAN_host_send_gcode_2params('M', 919, 'O', off, 'P' , Hysteresis End); + CAN_host_send_gcode_2params('M', 919, 'S', Hysteresis Start, 0, 0); + */ + + #if ALL(USE_CONTROLLER_FAN, CONTROLLER_FAN_EDITABLE) + CAN_host_send_gcode_2params('M', 710, 'E', controllerFan.settings.extruder_auto_fan_speed, 'P', controllerFan.settings.probing_auto_fan_speed); + #endif + + // Signal to the toolhead that the configuration is complete, use it as the last Gcode to send + if (changeStatus) + CAN_host_send_gcode_2params('M', CAN_HOST_CONFIGURATION_COMPLETE, 0, 0, 0, 0); +} + +void CAN_host_idle() { // Tasks that can/should not be done in the ISR + + if (CAN_time_sync_request) { // Send time sync timestamps + CAN_time_sync_request = false; + CAN_host_send_timestamp(); + } + + if (string_message_complete) { // Received string message is complete, display the string + BUZZ(1, SOUND_OK); + SERIAL_ECHOPGM(">>> CAN toolhead MSG: "); + + string_message.echo(); // Show received string message, ends on '\n' + + string_message_complete = false; // Get ready for the next string + string_message.clear(); + } + + // Report time sync results + if ((hCAN1.ErrorCode || CAN_toolhead_error || HAL_CAN_error_code) && ELAPSED(millis(), CAN_next_error_message_time)) { + + BUZZ(1, SOUND_ERROR); + + if (CAN_toolhead_error) { + SERIAL_ECHOLNPGM(">>> CAN Error reported by toolhead"); + CAN_toolhead_error = false; // Reset, but will be repeated by the toolhead + } + + if (HAL_CAN_error_code) + SERIAL_ECHOLNPGM(">>> HAL CAN Error reported: ", HAL_CAN_error_code); + + if (CAN_host_error_code) + SERIAL_ECHOLNPGM(">>> HOST CAN Error Code: ", CAN_host_error_code); + + if (hCAN1.ErrorCode) + SERIAL_ECHOLNPGM(">>> CAN Error Code = ", hCAN1.ErrorCode); + + CAN_next_error_message_time = millis() + CAN_HOST_ERROR_REPEAT_TIME; + } + + if (ELAPSED(millis(), CAN_next_temp_report_time)) { + + CAN_next_temp_report_time = millis() + CAN_HOST_ERROR_REPEAT_TIME; + if (first_E0_error) { // Send error notification + BUZZ(1, SOUND_ERROR); // Warn with sound + SERIAL_ECHOLNPGM("Error: No CAN E0 temp updates"); + } + else // Send only error message + SERIAL_ECHOLNPGM(">>> CAN error: No E0 temp updates"); + + first_E0_error = false; // Warn only once + + #if DISABLED(CAN_DEBUG) // Only kill if not debugging + kill(F("CAN error: No E0 tempeature updates")); + #endif + + if (CAN_toolhead_setup_request) // The toolhead requested the setup configuration + CAN_host_send_setup(true); + } +} + +HAL_StatusTypeDef CAN_host_send_gcode() { // Forward a Marlin Gcode via CAN (uses parser.command_letter, Gcode_no, parser.value_float()) + // Send a Gcode to the toolhead with parameters and values + // Gcode starts with extended frame which can send the Gcode with max 2 parameters and values. + // Extended frames are send to complete all parameters and values (max 2 per extended message). + // 1. Analyze Gcode command + // 2. Ignore gcodes that do not need to be forwarded + // 3. Send parameters and values + // char s[] = "G0 X123.45678 Y124.45678 Z125.45678 E126.45678 F127.45678\n"; + + HAL_StatusTypeDef status = HAL_OK; + uint32_t TxMailbox; // Stores which Mailbox (0-2) was used to store the sent message + + if (parser.command_letter != 'M') // Only forward Mxxx Gcode to toolhead + return HAL_OK; + + uint32_t Gcode_type = CAN_ID_GCODE_TYPE_M; // M-code, fixed for now + uint32_t Gcode_no = parser.codenum & CAN_ID_GCODE_NUMBER_MASK; + + if (Gcode_no == 109) // Convert M109(Hotend wait) to M104 (no wait) to keep the toolhead responsive + Gcode_no = 104; + + if ((Gcode_no == 501) || (Gcode_no == 502)) // M501=Restore settings, M502=Factory defaults + CAN_toolhead_setup_request = true; // Also update settings for the toolhead + + if ((Gcode_no != 104) && // Set hotend target temp + (Gcode_no != 106) && // Set cooling fan speed + (Gcode_no != 107) && // Cooling fan off + (Gcode_no != 115) && // Firmware info (testing) + (Gcode_no != 119) && // Endstop status (testing) + (Gcode_no != 150) && // Set NeoPixel values + //(Gcode_no != 108) && // Break and Continue + (Gcode_no != 280) && // Servo position + (Gcode_no != 306) && // MPC settings/tuning + (Gcode_no != 710) && // Control fan PWM + (Gcode_no != 997)) // Reboot + return HAL_OK; // Nothing to do + + uint32_t index; + uint32_t parameter_counter = 0; + char letters[] = "XYZEFPSABCHIJKLOQRTUVW"; // All possible parameters (22), defines scan order, no "D G M N", includes 'T' for autotune (M306 T) + static uint32_t parameters[8] = { 0 }; // Store found parameters, send max 7 parameters (send in pairs, so reserve 8), CodeA=1 (ASCII65), CodeE=5, CodeF=6, CodeX=88-64=24, CodeY=89-64=25, CodeZ=90-64=26 + static float values[8] = { 0 }; // Store found values, send max 7 parameters (send in pairs, so reserve 8) + + uint8_t CAN_tx_buffer[8]; // 8 bytes CAN data TX buffer + + /* + switch (parser.command_letter) // Filter/adjust Gcodes + { + case 'G': Gcode_type = CAN_ID_GCODE_TYPE_G; + switch (Gcode_no) + { + case 12: break; // No Nozzle cleaning support needed on toolhead + case 29: case 34: return HAL_OK; // No bedleveling/Z-syncing on toolhead + break; + } + break; + + case 'M': Gcode_type = CAN_ID_GCODE_TYPE_M; + switch (Gcode_no) + { // Save Prog mem: M112, M48, M85, M105, M114, M155, M500, M501, M502, M503, M226, M422 + case 109: Gcode_no = 104; break; // Replace M109 with M104 + case 112: Gcode_no = 104; break; // Don't shutdown board, should stop heating with "M104" + + case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 524: case 540: case 928: + case 27: case 28: case 29: case 30: case 32: case 33: case 34: // No SD file commands + case 43: // No pin debug + case 48: // No repeatability test + case 85: // No inactivity shutdown + case 100: // No show free memory support + case 108: // Break and Continue + case 105: // No temperature reporting + case 114: // Don't report position + case 117: case 118: case 119: // Don't send strings + case 140: case 190: // Ignore bed temp commands + case 150: // Set NeoPixel values + case 154: // No auto position reporting + case 155: // No tempeature reporting + case 226: // Wait for pin state + case 240: // No camera support + case 250: // No LCD contrast support + case 260: case 261: // No I2C on toolhead + case 280: // Don't send servo angle, done via Servo.cpp already + case 290: // No baby stepping + case 300: // No tones + // case 303: // No PID autotune (done on TOOLHEAD) + case 304: // No bed PID settings + // case 306: // MPC autotune (done on TOOLHEAD) + case 350: case 351: // No live microstepping adjustment + case 380: case 381: // No solenoid support + case 401: case 402: // No probe deploy/stow, done via M280 servo angles + case 412: // Filament runout sensor done by MASTER + case 420: case 421: // No bed leveling state + case 423: // No X Twist Compensation + case 425: // No backlash compensation + case 500: case 501: case 502: case 503: case 504: case 910: // No EEPROM on toolhead, remove M50x commands to save Prog mem + case 510: case 511: case 512: // No locking of the machine + case 605: // No IDEX commands + case 810: case 811: case 812: case 813: case 814: case 815: case 816: case 817: case 818: case 819: + case 851: // + case 871: // No Probe temp config + case 876: // No handle prompt response + case 913: // No Set Hybrid Threshold Speed + case 914: // No TMC Bump Sensitivity + case 997: // No remote reset + case 998: // No ESP3D reset + return HAL_OK; // NO CAN MESSAGE + } + break; + + case 'T': Gcode_type = CAN_ID_GCODE_TYPE_T; + switch (Gcode_no) + { + case 0: case 1: + break; + } + break; + + case 'D': Gcode_type = CAN_ID_GCODE_TYPE_D; + switch (Gcode_no) + { + case 0: case 1: + break; + } + break; + default: return HAL_OK; // Invalid command, nothing to do + } + */ + + #if ENABLED(CAN_DEBUG) + SERIAL_ECHOPGM(">>> CAN Gcode to toolhead: "); + SERIAL_CHAR(parser.command_letter); + SERIAL_ECHO(Gcode_no); + #endif + + if (strlen(parser.command_ptr) > 4) // "M107\0", Only scan for parameters if the string is long enough + for (index = 0; index < sizeof(letters); index++) { // Scan parameters + if (parser.seen(letters[index])) { + parameters[parameter_counter] = (letters[index] - 64) & CAN_ID_PARAMETER_LETTER_MASK; // Store parameter letter, A=1, B=2... + + #if ENABLED(CAN_DEBUG) + SERIAL_CHAR(' ', letters[index]); + #endif + + if (parser.has_value()) { // Check if there is a value + values[parameter_counter++] = parser.value_float(); + + #if ENABLED(CAN_DEBUG) + if (values[parameter_counter - 1] == int(values[parameter_counter - 1])) + SERIAL_ECHO(i16tostr3left(values[parameter_counter - 1])); // Integer value + else + SERIAL_ECHO(p_float_t(values[parameter_counter - 1], 4)); // Float with 4 digits + #endif + } + else // No value for parameter + values[parameter_counter++] = NAN; // Not A Number, indicates no parameter value is present + } + + if (parameter_counter == 8) { // Max is 7 parameters + CAN_host_error_code |= CAN_ERROR_HOST_INVALID_GCODE; + parameter_counter--; + SERIAL_ECHOLNPGM("\nError: TOO MANY PARAMETERS (> 7): ", parser.command_ptr); + BUZZ(1, SOUND_ERROR); + break; + } + } + + #if ENABLED(CAN_DEBUG) + SERIAL_EOL(); + #endif + + parameters[parameter_counter] = 0; // Set next parameter to 0 (0=no parameter), send in pairs + index = 0; + float * fp = (float *)CAN_tx_buffer; // Points to TX buffer + + TxHeader.IDE = CAN_ID_EXT; // Start Gcode with Extended ID, then send Standard ID messages if there are more than 2 parameters + + uint32_t deadline = millis() + CAN_HOST_MAX_WAIT_TIME; // Record message send start time + do { + TxHeader.DLC = MIN(8, (parameter_counter - index) << 2); // Maximum 8 bytes, 4 bytes --> only 1 parameter, 8 bytes --> 2 parameters + + if (TxHeader.IDE == CAN_ID_EXT) + TxHeader.ExtId = CAN_set_extended_id(Gcode_type, Gcode_no, parameters[index], parameters[index + 1], parameter_counter); + else { + CAN_host_FIFO_toggle_bit = !CAN_host_FIFO_toggle_bit; + TxHeader.StdId = (CAN_host_FIFO_toggle_bit ? STDID_FIFO_TOGGLE_BIT : 0) | // Toggle bit + (parameters[index ] << CAN_ID_PARAMETER1_BIT_POS) | // Parameter 1 + (parameters[index + 1] << CAN_ID_PARAMETER2_BIT_POS); // Parameter 2 + } + + *fp++ = values[index++]; // Copy first parameter value to data, move pointer to next 4 bytes + *fp-- = values[index++]; // Copy 2nd parameter value to data, move pointer to beginning of data array for next round + + while ((HAL_CAN_GetTxMailboxesFreeLevel(&hCAN1) == 0) && PENDING(millis(), deadline)) { /* BLOCKING! Wait for emtpy TX buffer */ } + + if (HAL_CAN_GetTxMailboxesFreeLevel(&hCAN1)) + status = HAL_CAN_AddTxMessage(&hCAN1, &TxHeader, CAN_tx_buffer, &TxMailbox); // Queue CAN message + else + CAN_host_error_code |= CAN_ERROR_HOST_TX_MSG_DROPPED; + + if (status != HAL_OK) return status; + + TxHeader.IDE = CAN_ID_STD; // All following messages have standard ID for parameter values, 11 bits identifier + } while (index < parameter_counter); + + return status; + +} // CAN_host_send_gcode + +void CAN_host_send_position() { // Send the X, Y, Z and E position to the TOOLHEAD + CAN_host_send_gcode_2params('G', 92, 'X', current_position.x, 'Y', current_position.y); // M92 X Y + CAN_host_send_gcode_2params('G', 92, 'Z', current_position.z, 'E', current_position.e); // M92 E Z +} + +// Enable a GPIO clock based on the GPIOx address for STM32F4 +void gpio_clock_enable(GPIO_TypeDef *regs) +{ + uint32_t pos = ((uint32_t)regs - GPIOA_BASE) >> 10; + RCC->AHB1ENR |= (1 << pos); + RCC->AHB1ENR; +} + +void HAL_CAN_MspInit(CAN_HandleTypeDef* canHandle) { // Called by HAL_CAN_Init + + if (canHandle->Instance == CAN1) + __HAL_RCC_CAN1_CLK_ENABLE(); // Enable CAN1 clock + + if (canHandle->Instance == CAN2) + __HAL_RCC_CAN2_CLK_ENABLE(); // Enable CAN2 clock + + // Use some macros to find the required setup info based on the provided CAN pins + uint32_t _CAN_RD_pin = digitalPinToPinName(CAN_RD_PIN); + uint32_t _CAN_TD_pin = digitalPinToPinName(CAN_TD_PIN); + uint32_t _CAN_RD_function = pinmap_find_function(digitalPinToPinName(CAN_RD_PIN), PinMap_CAN_RD); + uint32_t _CAN_TD_function = pinmap_find_function(digitalPinToPinName(CAN_TD_PIN), PinMap_CAN_TD); + + // Enable the GPIOx device related to the CAN_RD_pin + gpio_clock_enable(get_GPIO_Port(STM_PORT(_CAN_RD_pin))); + + // CAN1 GPIO Configuration + // PB8 ------> CAN1_RX + // PB9 ------> CAN1_TX + + GPIO_InitTypeDef GPIO_InitStruct = { 0 }; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; + GPIO_InitStruct.Pull = GPIO_NOPULL; + + //GPIO_InitStruct.Pin = GPIO_PIN_8; // Pin PB8 and Pin PB9 + GPIO_InitStruct.Pin = STM_GPIO_PIN(STM_PIN(_CAN_RD_pin)); + //GPIO_InitStruct.Alternate = GPIO_AF9_CAN1; + GPIO_InitStruct.Alternate = STM_PIN_AFNUM(_CAN_RD_function); + //HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + HAL_GPIO_Init(get_GPIO_Port(STM_PORT(_CAN_RD_pin)), &GPIO_InitStruct); + + // Split pin initialisation, perhaps not needed + gpio_clock_enable(get_GPIO_Port(STM_PORT(_CAN_TD_pin))); + //GPIO_InitStruct.Pin = GPIO_PIN_9; // Pin PB8 and Pin PB9 + GPIO_InitStruct.Pin = STM_GPIO_PIN(STM_PIN(_CAN_TD_pin));; + //GPIO_InitStruct.Alternate = GPIO_AF9_CAN1; + GPIO_InitStruct.Alternate = STM_PIN_AFNUM(_CAN_TD_function); + //HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); + HAL_GPIO_Init(get_GPIO_Port(STM_PORT(_CAN_TD_pin)), &GPIO_InitStruct); + + // Enable the CAN interrupt handlers + HAL_NVIC_SetPriority(CAN1_RX0_IRQn, 1, 1); + HAL_NVIC_EnableIRQ(CAN1_RX0_IRQn); // Enable CAN1 FIFO0 interrupt handler + + HAL_NVIC_SetPriority(CAN1_RX1_IRQn, 1, 1); // Set CAN interrupt priority + HAL_NVIC_EnableIRQ(CAN1_RX1_IRQn); // Enable CAN1 FIFO1 interrupt handler +} + +HAL_StatusTypeDef CAN_host_stop() { + return HAL_CAN_Stop(&hCAN1); +} + +int seg1_encode(uint32_t s) { // Ds must be between 1 and 16 +// Timing is encoded in 4 bits with an offset of 1 +// 4 bits: CAN_BTR_TS1_3 : CAN_BTR_TS1_2 : CAN_BTR_TS1_1 : CAN_BTR_TS1_0 + return (--s << CAN_BTR_TS1_Pos); +} + +int seg2_encode(uint32_t s) { // Must be between 1 and 8 +// Timing is encoded in 3 bits with an offset of 1 +// 3 bits: CAN_BTR_TS2_2 : CAN_BTR_TS1_1 : CAN_BTR_TS1_0 + return (--s << CAN_BTR_TS2_Pos); +} + +// Calculate the CAN sample timing, seg1 and seg2, sjw = 1 (no baudrate switching) +// seg1 range: 1-16, seg2 range: 1-8, sjw = 1, so minimum is 3, use a max of 19 clocks per bit +// Will give solutions for all "reasonable" clock rates +int CAN_calculate_segments(uint32_t *seg1, uint32_t *seg2, uint32_t *prescaler) { + + uint32_t CAN_clock = HAL_RCC_GetPCLK1Freq(); // APB1 clock (=PCLK1 clock) + float clocks_per_bit; + *prescaler = 1; + + do { // Check seg1 and seg2 range + do { // Check prescaler + clocks_per_bit = (float)CAN_clock / (*prescaler * CAN_BAUDRATE); // Clocks per bit must be a whole number + } while (((clocks_per_bit != (uint32_t)clocks_per_bit) || (clocks_per_bit > 19)) && (clocks_per_bit > 3) && (*prescaler)++); + + if (clocks_per_bit < 4) // Minimal 4 clocks per bit (SJW + SEG1 + SEG2 = 1 + 1 + 1) + return -1; // Baudrate is not possible + + *seg2 = (clocks_per_bit / 8) + 0.5; // Preferred sample point at 87.5% (7/8) + *seg1 = (int)clocks_per_bit - *seg2 - 1; // sjw = 1; + + } while ((*seg1 + *seg2 + 1 > 19) && (*prescaler)++); // MAX 16 + 2 + 1 clocks + + *seg1 = (*seg1 - 1) << CAN_BTR_TS1_Pos; // Convert to register values + *seg2 = (*seg2 - 1) << CAN_BTR_TS2_Pos; // Convert to register values + + return HAL_OK; +} + +HAL_StatusTypeDef CAN_host_start() { + + HAL_StatusTypeDef status = HAL_OK; + + // The CAN clock must be set first because the sample timing depends on it + __HAL_RCC_CAN1_CLK_ENABLE(); + + // Initialize TxHeader with constant values + TxHeader.ExtId = 0; + TxHeader.StdId = 0; + TxHeader.RTR = CAN_RTR_DATA; // Data transmission type: CAN_RTR_DATA / CAN_RTR_REMOTE + TxHeader.TransmitGlobalTime = DISABLE; // Put timestamp in Data[6-7], requires Time Triggered Communication Mode + +uint32_t seg1, seg2, prescaler; +if (CAN_calculate_segments(&seg1, &seg2, &prescaler) != HAL_OK) { + SERIAL_ECHOLNPGM("Impossible CAN baudrate, check CAN clock and baudrate"); + return HAL_ERROR; +} + + // CAN peripheral clock is 42MHz (168Mhz / 4) + // CAN baud rate = clock frequency / clock divider / prescaler / (1 + TSG1 + TSG2) + // Baud rate = 42M / 3 / 1 / (1 + 11 + 2) = 1M baud (Sample point = 12/14=86%) + // Baud rate = 42M / 3 / 2 / (1 + 11 + 2) = 500k baud + // Baud rate = 42M / 3 / 4 / (1 + 11 + 2) = 250k baud + hCAN1.Instance = CAN1; + hCAN1.Init.Prescaler = prescaler; // 1-1024, 42MHz peripheral clock / 3 --> 14MHz -> 1M baud. 6 --> 500K baud. 12 --> 250K baud. + hCAN1.Init.AutoBusOff = DISABLE; // DISABLE: Software controlled Bus-off. ENABLE: Automatic hardware controlled (no send/receive) + hCAN1.Init.AutoWakeUp = ENABLE; // ENABLE: Automatic hardware controlled bus wakeup. DISABLE: Software controlled bus wakeup. + hCAN1.Init.AutoRetransmission = ENABLE; // DISABLE / ENABLE, resend if transmission failed, but locks up if communication fails/cable not connected!!!!!!!!!!!!!!!!! + hCAN1.Init.SyncJumpWidth = CAN_SJW_1TQ; // CAN_SJW_1TQ (1-4) Should be 1 + hCAN1.Init.TimeSeg1 = seg1; // CAN_BS1_11TQ (1-16) + hCAN1.Init.TimeSeg2 = seg2; // CAN_BS2_2TQ (1-8) + hCAN1.Init.Mode = CAN_MODE_NORMAL; // CAN_MODE_NORMAL / CAN_MODE_SILENT / CAN_MODE_LOOPBACK / CAN_MODE_SILENT_LOOPBACK + hCAN1.Init.TimeTriggeredMode = DISABLE; // TTCAN is used to assign timeslot to the devices for real time applications + hCAN1.Init.ReceiveFifoLocked = DISABLE; // Handle RX FIFO overruns. DISABLE: Overwrite previous message with new one. ENABLE: Discard the new message. + hCAN1.Init.TransmitFifoPriority = ENABLE; // Handle TX FIFO send order. ENABLE: Chronologically. DISABLE: Transmit lower ID number first. + + status = HAL_CAN_Init(&hCAN1); // Calls HAL_CAN_Init + if (status != HAL_OK) return status; + + CAN_FilterTypeDef sFilterConfig; + + // Store CAN messags with highest bit of StdId set in FIFO0 + sFilterConfig.FilterBank = 0; // This filter bank ID number (0-13 for single CAN instances) + sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // Accept if "Received ID" & Mask = ID, CAN_FILTERMODE_IDMASK / CAN_FILTERMODE_IDLIST (See Figure 342 in RM0090) + sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // CAN_FILTERSCALE_16BIT / CAN_FILTERSCALE_32BIT (See Figure 342 in RM0090) + sFilterConfig.FilterIdHigh = 0b1000000000000000; // ID MSB: (0-0xFFFF) (StdId[10-0] [ExtId17-13]) (See Figure 342 in RM0090) + sFilterConfig.FilterIdLow = 0; // ID LSB: (0-0xFFFF) ([ExtId12-0][IDE][RTR] 0) (0="don't care") + sFilterConfig.FilterMaskIdHigh = 0b1000000000000000; // Mask MSB: (0-0xFFFF) (StdId[10-0] [ExtId17-13]) (See Figure 342 in RM0090) + sFilterConfig.FilterMaskIdLow = 0; // Mask LSB: (0-0xFFFF) ([ExtId12-0][IDE][RTR] 0) (0="don't care") + sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO0; // Store message in FIFO1 (CAN_FILTER_FIFO0 / CAN_FILTER_FIFO1) + sFilterConfig.FilterActivation = CAN_FILTER_ENABLE; // CAN_FILTER_ENABLE / CAN_FILTER_DISABLE + sFilterConfig.SlaveStartFilterBank = 0; // Start bank number for CAN slave instance (not used in single CAN setups) + status = HAL_CAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + + // Store all remaining CAN messages in FIFO1 + sFilterConfig.FilterBank = 1; // This filter bank ID number (0-13 for single CAN instances) +//sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // CAN_FILTERMODE_IDMASK / CAN_FILTERMODE_IDLIST (See Figure 342 in RM0090) +//sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // CAN_FILTERSCALE_16BIT / CAN_FILTERSCALE_32BIT (See Figure 342 in RM0090) + sFilterConfig.FilterIdHigh = 0; // ID MSB: (0-0xFFFF) (StdId[10-0] [ExtId17-13]) (See Figure 342 in RM0090) +//sFilterConfig.FilterIdLow = 0; // ID LSB: (0-0xFFFF) ([ExtId12-0][IDE][RTR] 0) (0="don't care") + sFilterConfig.FilterMaskIdHigh = 0; // Mask MSB: (0-0xFFFF) (StdId[10-0] [ExtId17-13]) (See Figure 342 in RM0090) +//sFilterConfig.FilterMaskIdLow = 0; // Mask LSB: (0-0xFFFF) ([ExtId12-0][IDE][RTR] 0) (0="don't care") + sFilterConfig.FilterFIFOAssignment = CAN_FILTER_FIFO1; // Store message in FIFO0 (CAN_FILTER_FIFO0 / CAN_FILTER_FIFO1) +//sFilterConfig.FilterActivation = CAN_FILTER_ENABLE; // CAN_FILTER_ENABLE / CAN_FILTER_DISABLE +//sFilterConfig.SlaveStartFilterBank = 0; // Start bank number for CAN slave instance (not used in single CAN setups) + status = HAL_CAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + + // Activate RX FIFO0/FIFO1 new message interrupt + status = HAL_CAN_ActivateNotification(&hCAN1, CAN_IT_RX_FIFO0_MSG_PENDING); // Calls CAN1_RX0_IRQHandler / CAN2_RX0_IRQHandler + if (status != HAL_OK) return status; + + status = HAL_CAN_ActivateNotification(&hCAN1, CAN_IT_RX_FIFO1_MSG_PENDING); // Calls CAN1_RX1_IRQHandler / CAN2_RX0_IRQHandler + if (status != HAL_OK) return status; + + // Activate RX FIFO0/FIFO1 overrun interrupt + status = HAL_CAN_ActivateNotification(&hCAN1, CAN_IT_RX_FIFO0_OVERRUN); // Calls CAN1_RX0_IRQHandler / CAN2_RX0_IRQHandler + if (status != HAL_OK) return status; + + status = HAL_CAN_ActivateNotification(&hCAN1, CAN_IT_RX_FIFO1_OVERRUN); // Calls CAN1_RX1_IRQHandler / CAN2_RX0_IRQHandler + if (status != HAL_OK) return status; + + status = HAL_CAN_ActivateNotification(&hCAN1, CAN_IT_ERROR); // Calls CAN1_RX0_IRQHandler / CAN2_RX0_IRQHandler + if (status != HAL_OK) return status; + + status = HAL_CAN_Start(&hCAN1); // Start the CAN module + if (status != HAL_OK) return status; + + #ifdef CAN_LED_PIN + pinMode(CAN_LED_PIN, OUTPUT); + #endif + + status = CAN_host_send_gcode_2params('M', 997, 0, 0, 0, 0); // M997, reset toolhead at host startup + + return status; +}// CAN_host_start() + +void CAN_host_read_message(CAN_HandleTypeDef *hcan, uint32_t RxFifo) { // ISR! FIFO 0/1 CAN message interrupt handler + + CAN_RxHeaderTypeDef RxHeader; + uint8_t CAN_RX_buffer_FIFO[8]; // CAN message buffer + + if (HAL_CAN_GetRxFifoFillLevel(hcan, RxFifo) && + (HAL_CAN_GetRxMessage(hcan, RxFifo, &RxHeader, CAN_RX_buffer_FIFO) == HAL_OK)) { + + if ((RxHeader.StdId & CAN_ID_IO_MASK) != CAN_io_state) { // First handle time critical virtual IO update + CAN_io_state = (RxHeader.StdId & CAN_ID_IO_MASK); + endstops.update(); + } + + if (RxHeader.StdId & CAN_ID_STRING_MESSAGE_BIT_MASK) { // Toolhead sent a string message + char *CAN_RX_p = (char *)CAN_RX_buffer_FIFO; + for (uint32_t i = 0; i < RxHeader.DLC; i++) + string_message.append(CAN_RX_p[i]); // Copy message to global buffer + + if (CAN_RX_p[RxHeader.DLC - 1] == '\n') + string_message_complete = true; // String is complete, idle task can show the string + } + else if (RxHeader.DLC == 4) { // Only 1 record, so it's a temperature update (DLC = Data Length Code is 4 bytes) + float *fp = (float *)CAN_RX_buffer_FIFO; + thermalManager.temp_hotend[0].celsius = *fp; // Set E0 hotend temperature from received message + CAN_next_temp_report_time = millis() + CAN_HOST_E0_TEMP_UPDATE_WATCHDOG_TIME; // A temp update must be received within this window + first_E0_error = true; // Reset error status + } + + if (RxHeader.StdId & CAN_ID_REQUEST_TIME_SYNC_BIT_MASK) { // Toolhead signals request for time stamp + time_sync_request_time = micros(); // Record the time sync request receive time + CAN_time_sync_request = true; + } + + CAN_toolhead_setup_request = (RxHeader.StdId & CAN_ID_REQUEST_SETUP_BIT_MASK) > 0; // Toolhead requests setup configuration + + CAN_toolhead_error = (RxHeader.StdId & CAN_ID_ERROR_BIT_MASK) > 0; // Toolhead signals an error + } +} + +void CAN1_RX0_IRQHandler() { // ISR! CAN FIFO0 interrupt handler (overrides weak function) + + HAL_CAN_IRQHandler(&hCAN1); // Forward call for callbacks --> HAL_CAN_RxFifo0MsgPendingCallback/HAL_CAN_ErrorCallback + // OR + //HAL_CAN_RxFifo0MsgPendingCallback(&hCAN1); // Call the required callback directly, faster but no error reporting +} + +void CAN1_RX1_IRQHandler() { // ISR! CAN FIFO1 Interrupt handler (overrides weak function) + + HAL_CAN_IRQHandler(&hCAN1); // Forward call for callbacks --> HAL_CAN_RxFifo1MsgPendingCallback/HAL_CAN_ErrorCallback + // OR + //HAL_CAN_RxFifo1MsgPendingCallback(&hCAN1); // Call the required callback directly, faster but no error reporting +} + +void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { // ISR! New FIFO0 message interrupt handler + CAN_host_read_message(hcan, CAN_RX_FIFO0); +} + +void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan) { // ISR! New FIFO1 message interrupt handler + CAN_host_read_message(hcan, CAN_RX_FIFO1); +} + +void HAL_CAN_ErrorCallback(CAN_HandleTypeDef *hcan) { // ISR! Interrupt handler for any CAN error + HAL_CAN_error_code = hcan->ErrorCode; // Store the received error code +} + +#endif // CAN_HOST && STM32F4xx diff --git a/Marlin/src/HAL/STM32/FDCAN_host.cpp b/Marlin/src/HAL/STM32/FDCAN_host.cpp new file mode 100644 index 000000000000..b962bd120421 --- /dev/null +++ b/Marlin/src/HAL/STM32/FDCAN_host.cpp @@ -0,0 +1,879 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/** + * Contributor Notes: + * NOTE 1: Requires `HAL_FDCAN_MODULE_ENABLED`, e.g., with `-DFDHAL_CAN_MODULE_ENABLED` + * For Arduino IDE use "hal_conf_extra.h" with `#define HAL_FDCAN_MODULE_ENABLED` + * NOTE 2: Serial communication in ISR causes issues! Hangs etc. so avoid this! + */ + +#include "../../inc/MarlinConfigPre.h" + +#if ALL(CAN_HOST, STM32H7xx) + +#include "../platforms.h" +#include "../../gcode/parser.h" +#include "../../module/temperature.h" +#include "../../module/motion.h" // For current_position variable +#include "../../module/planner.h" // For steps/mm parameters variables +#include "../../feature/tmc_util.h" +#include "../../module/endstops.h" +#include "../../feature/controllerfan.h" // For controllerFan settings +#include "../../libs/numtostr.h" // For float to string conversion + +#include "../shared/CAN.h" + +// Interrupt handlers +extern "C" void FDCAN1_IT0_IRQHandler(void); +extern "C" void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs); +extern "C" void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs); + +#ifndef CAN_BAUDRATE + #define CAN_BAUDRATE 1000000LU +#endif + +#ifndef CAN_RD_PIN + #define CAN_RD_PIN PB8 +#endif +#ifndef CAN_TD_PIN + #define CAN_TD_PIN PB9 +#endif + +#if (CAN_BAUDRATE != 1000000) && (CAN_BAUDRATE != 500000) && (CAN_BAUDRATE != 250000) && (CAN_BAUDRATE != 125000) + #error ERROR: Select a valid CAN_BAUDRATE: 1000000, 500000, 250000 or 125000 baud +#endif + +#define FDCAN_HOST_TX_FIFO_DEPTH 8 // TX FIFO0 and FIFO1 depth 0-32 +#define FDCAN_HOST_RX_FIFO_DEPTH 8 // RX FIFO0 and FIFO1 depth 0-64 + +#define FDCAN_HOST_DATALENGTH_OFFSET 16 // Bit offset of FDCAN_DLC_BYTES_1 in register + +FDCAN_HandleTypeDef hCAN1 = { 0 }; // The global FDCAN handle +FDCAN_TxHeaderTypeDef TxHeader = { 0 }; // Header to send a FDCAN message +volatile uint32_t CAN_io_state = 0; // Virtual IO state variable +volatile bool CAN_toolhead_error = 0; // Register if an error was reported by the toolhead +volatile bool CAN_toolhead_setup_request = false; // Signals the toolhead requested setup information +volatile bool CAN_time_sync_request = false; // Signals the toolhead requested a time sync +volatile uint32_t time_sync_request_time = 0; // Record the time the time sync request was received + +volatile uint32_t HAL_FDCAN_error_code = 0; // Store the HAL FDCAN error code +volatile uint32_t CAN_host_error_code = 0; // Store the CAN host error code + +volatile bool first_E0_error = true; // First CAN bus error, show warning only once +volatile bool string_message_complete = false; // Signals a complete string message was received +uint32_t CAN_next_temp_report_time = 10000; // Track when the next toolhead temperature report should arrive, delay at startup +uint32_t CAN_next_error_message_time = 0; // Track when to display the next repeat of an error message +volatile bool CAN_host_FIFO_toggle_bit = false; // FIFO toggle flag for receiver FIFO filtering +SString string_message; // CAN string message buffer for incoming messages + +uint32_t CAN_host_get_iostate() { + return CAN_io_state; +} + +uint32_t CAN_set_extended_id(int gcode_type, int gcode_no, int parameter1, int parameter2, int count) { + + CAN_host_FIFO_toggle_bit = !CAN_host_FIFO_toggle_bit; // FIFO toggle bit + + return (CAN_host_FIFO_toggle_bit ? EXTID_FIFO_TOGGLE_BIT : 0) | // FIFO toggle bit + (count << CAN_ID_PARAMETER_COUNT_BIT_POS) | // Parameter count + (gcode_type << CAN_ID_GCODE_TYPE_BIT_POS) | // G/M/T/D-code + ((gcode_no & CAN_ID_GCODE_NUMBER_MASK) << CAN_ID_GCODE_NUMBER_BIT_POS) | // Gcode number + ((parameter1 & CAN_ID_PARAMETER_LETTER_MASK) << CAN_ID_PARAMETER1_BIT_POS) | // First parameter + ((parameter2 & CAN_ID_PARAMETER_LETTER_MASK) << CAN_ID_PARAMETER2_BIT_POS); // Second parameter + +} + +// Send time sync timestamp of arrival and response time +void CAN_host_send_timestamp() { // Request receive timestamp + request response timestamp + + TxHeader.IdType = FDCAN_EXTENDED_ID; + TxHeader.DataLength = FDCAN_DLC_BYTES_8; // Send sync time t1(receive time, uint32_t) and t2(response time, uint32_t) + TxHeader.Identifier = CAN_set_extended_id(CAN_ID_GCODE_TYPE_M, CAN_HOST_GCODE_TIME_SYNC_NO, 1, 2, 2); + + uint8_t CAN_tx_buffer[8]; // 8 bytes CAN data TX buffer + uint32_t * uint32p = (uint32_t *)CAN_tx_buffer; // Point to TX buffer + *uint32p++ = time_sync_request_time; + + uint32_t deadline = millis() + CAN_HOST_MAX_WAIT_TIME; + while ((HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1) < FDCAN_HOST_TX_FIFO_DEPTH) && PENDING(millis(), deadline)) { /* BLOCKING! Wait for empty TX buffer */ } + + if (HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1)) { + *uint32p = micros(); // Only record the response time at the last possible moment + HAL_FDCAN_AddMessageToTxFifoQ(&hCAN1, &TxHeader, CAN_tx_buffer); // Queue CAN message + } + else + CAN_host_error_code |= CAN_ERROR_HOST_TX_MSG_DROPPED; + +} + +// Send specified Gcode with max 2 parameters and 2 values via CAN bus +HAL_StatusTypeDef CAN_host_send_gcode_2params(uint32_t Gcode_type, uint32_t Gcode_no, uint32_t parameter1, float value1, uint32_t parameter2, float value2) { + + HAL_StatusTypeDef status = HAL_OK; + + switch (Gcode_type) { + + case 'D': + Gcode_type = CAN_ID_GCODE_TYPE_D; + return HAL_ERROR; + break; + + case 'G': + Gcode_type = CAN_ID_GCODE_TYPE_G; + return HAL_ERROR; + break; + + case 'M': + Gcode_type = CAN_ID_GCODE_TYPE_M; + + #if ENABLED(CAN_DEBUG) + SERIAL_ECHOPGM("; MSG to toolhead: \"M", Gcode_no); + if (parameter1) { + SERIAL_CHAR(' ', parameter1); + if (value1 == int(value1)) + SERIAL_ECHO(int(value1)); // Integer value + else + SERIAL_ECHO(p_float_t(value1, 4)); // Float with 4 digits + } + + if (parameter2) { + SERIAL_CHAR(' ', parameter2); + if (value2 == int(value2)) + SERIAL_ECHO(int(value2)); // Integer value + else + SERIAL_ECHO(p_float_t(value2, 4)); // Float with 4 digits + } + SERIAL_ECHOLN("\""); + #endif + + break; + + case 'T': Gcode_type = CAN_ID_GCODE_TYPE_T; + return HAL_ERROR; + break; + + default: + return HAL_ERROR; // Unknown Gcode type + } + + if (parameter1 > 31) + parameter1 -= 64; // Format 'A' = 1, 'B' = 2, etc. + + if (parameter2 > 31) + parameter2 -= 64; // Format 'A' = 1, 'B' = 2, etc. + + TxHeader.IdType = FDCAN_EXTENDED_ID; + + TxHeader.DataLength = 4 * (!!parameter1 + !!parameter2) << FDCAN_HOST_DATALENGTH_OFFSET; // Amount of bytes to send (4 or 8) + TxHeader.Identifier = CAN_set_extended_id(Gcode_type, Gcode_no, parameter1, parameter2, !!parameter1 + !!parameter2); + + uint8_t CAN_tx_buffer[8]; // 8 bytes CAN data TX buffer + float * fp = (float *)CAN_tx_buffer; // Point to TX buffer + *fp++ = value1; + *fp = value2; + + const uint32_t deadline = millis() + CAN_HOST_MAX_WAIT_TIME; + while ((HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1) == 0) && PENDING(millis(), deadline)) { /* BLOCKING! Wait for empty TX buffer */ } + + if (HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1)) + status = HAL_FDCAN_AddMessageToTxFifoQ(&hCAN1, &TxHeader, CAN_tx_buffer); // Queue CAN message + else + CAN_host_error_code |= CAN_ERROR_HOST_TX_MSG_DROPPED; + + return status; +} + +void CAN_host_send_setup(bool changeStatus) { // Send setup to toolhead + + // NOTE: Sending many command too fast will cause a Marlin command buffer overrun at the toolhead, add delays if needed + CAN_toolhead_setup_request = false; + + SERIAL_ECHOLNPGM(">>> CAN: Sending configuration to toolhead..."); + + #if ENABLED(MPCTEMP) + // M306 MPC settings (managed by host) + MPC_t &mpc = thermalManager.temp_hotend[0].mpc; + + CAN_host_send_gcode_2params('M', 306, 'A', mpc.ambient_xfer_coeff_fan0, 'C', mpc.block_heat_capacity); // M306 R A + CAN_host_send_gcode_2params('M', 306, 'F', mpc.fanCoefficient(), 'H', mpc.filament_heat_capacity_permm); // M306 F H + CAN_host_send_gcode_2params('M', 306, 'P', mpc.heater_power, 'R', mpc.sensor_responsiveness); // M306 P C + + #endif + + //CAN_host_send_gcode_2params('M', 150, 0, 0, 0, 0); // M150, SWITCH NEOPIXEL OFF + + /* + extern Planner planner; // M92 Steps per mm + CAN_host_send_gcode_2params('M', 92, 'X', planner.settings.axis_steps_per_mm[X_AXIS], 'Y', planner.settings.axis_steps_per_mm[Y_AXIS]); + CAN_host_send_gcode_2params('M', 92, 'Z', planner.settings.axis_steps_per_mm[Z_AXIS], 'E', planner.settings.axis_steps_per_mm[E_AXIS]); + + // M200 Set filament diameter + CAN_host_send_gcode_2params('M', 200, 'S', parser.volumetric_enabled, 'D', LINEAR_UNIT(planner.filament_size[0])); + #if ENABLED(VOLUMETRIC_EXTRUDER_LIMIT) + CAN_host_send_gcode_2params('M', 200, 'L', LINEAR_UNIT(planner.volumetric_extruder_limit[0]) + #endif + + // M201 Max acceleration + CAN_host_send_gcode_2params('M', 201, 'X', planner.settings.max_acceleration_mm_per_s2[X_AXIS], 'Y', planner.settings.max_acceleration_mm_per_s2[Y_AXIS]); + CAN_host_send_gcode_2params('M', 201, 'Z', planner.settings.max_acceleration_mm_per_s2[Z_AXIS], 'E', planner.settings.max_acceleration_mm_per_s2[E_AXIS]); + + // M203 Max feedrate + CAN_host_send_gcode_2params('M', 203, 'X', planner.settings.max_feedrate_mm_s[X_AXIS], 'Y', planner.settings.max_feedrate_mm_s[Y_AXIS]); + CAN_host_send_gcode_2params('M', 203, 'Z', planner.settings.max_feedrate_mm_s[Z_AXIS], 'E', planner.settings.max_feedrate_mm_s[E_AXIS]); + + // M204 Accelerations in units/sec^2, ENABLED BECAUSE IT INFORMS THE TOOLHEAD THE CONFIGURATION WAS SENT + CAN_host_send_gcode_2params('M', 204, 'P', planner.settings.acceleration, 'R', planner.settings.retract_acceleration); + CAN_host_send_gcode_2params('M', 204, 'T', planner.settings.travel_acceleration, 0, 0); + + // M205 + #if ENABLED(CLASSIC_JERK) + CAN_host_send_gcode_2params('M', 205,'S')) planner.settings.min_feedrate_mm_s, 'T')) planner.settings.min_travel_feedrate_mm_s); + CAN_host_send_gcode_2params('M', 205, M205_MIN_SEG_TIME_PARAM, planner.settings.min_segment_time_us, 'J', planner.junction_deviation_mm); + CAN_host_send_gcode_2params('M', 205, 'X', LINEAR_UNIT(planner.max_jerk.x), 'Y', LINEAR_UNIT(planner.max_jerk.y)); + CAN_host_send_gcode_2params('M', 205, 'Z', LINEAR_UNIT(planner.max_jerk.z), 'E', LINEAR_UNIT(planner.max_jerk.e)); + CAN_host_send_gcode_2params('M', 205, 'J', LINEAR_UNIT(planner.junction_deviation_mm), 0, 0); + #endif + + // M206 Home offset + #if DISABLED(NO_HOME_OFFSETS) + CAN_host_send_gcode_2params('M', 206, 'X', LINEAR_UNIT(home_offset.x), 'Y', LINEAR_UNIT(home_offset.y)); + CAN_host_send_gcode_2params('M', 206, 'Z', LINEAR_UNIT(home_offset.z), 0, 0); + #endif + + // M207 Set Firmware Retraction + // M208 - Firmware Recover + // M209 - Set Auto Retract + + // M220 Speed/feedrate + CAN_host_send_gcode_2params('M', 220, 'S', feedrate_percentage, 0, 0); + + // M221 Flow percentage + CAN_host_send_gcode_2params('M', 221, 'T', 0, 'S', planner.flow_percentage[0]); + // CAN_host_send_gcode_2params('M', 221, 'T', 1, 'S', planner.flow_percentage[1]); // For 2nd extruder + + // M302 Cold extrude settings + #if ENABLED(PREVENT_COLD_EXTRUSION) + CAN_host_send_gcode_2params('M', 302, 'P', '0' + thermalManager.allow_cold_extrude, 'S', thermalManager.extrude_min_temp); // P0 enable cold extrusion checking, P1 = disabled, S=Minimum temperature + #endif + + // M569 TMC Driver StealthChop/SpreadCycle + CAN_host_send_gcode_2params('M', 569, 'S', stepperE0.get_stored_stealthChop(), 'E', 0); // M569 S[0/1] E + + // M592 Nonlinear Extrusion Control + + // M916 TMC Motor current + CAN_host_send_gcode_2params('M', 906, 'E', stepperE0.getMilliamps(), 0, 0); + + // M919 TMC Chopper timing for E only + CAN_host_send_gcode_2params('M', 919, 'O', off, 'P' , Hysteresis End); + CAN_host_send_gcode_2params('M', 919, 'S', Hysteresis Start, 0, 0); + */ + + #if ALL(USE_CONTROLLER_FAN, CONTROLLER_FAN_EDITABLE) + CAN_host_send_gcode_2params('M', 710, 'E', controllerFan.settings.extruder_auto_fan_speed, 'P', controllerFan.settings.probing_auto_fan_speed); + #endif + + // Signal to the toolhead that the configuration is complete, use it as the last Gcode to send + if (changeStatus) + CAN_host_send_gcode_2params('M', CAN_HOST_CONFIGURATION_COMPLETE, 0, 0, 0, 0); +} + +void CAN_host_idle() { // Tasks that can/should not be done in the ISR + + if (CAN_time_sync_request) { // Send time sync timestamps + CAN_time_sync_request = false; + CAN_host_send_timestamp(); + } + + if (string_message_complete) { // Received string message is complete, display the string + BUZZ(1, SOUND_OK); + SERIAL_ECHOPGM(">>> CAN toolhead MSG: "); + + string_message.echo(); // Show received string message, ends on '\n' + + string_message_complete = false; // Get ready for the next string + string_message.clear(); + } + + // Report time sync results + if ((hCAN1.ErrorCode || CAN_toolhead_error || HAL_FDCAN_error_code) && ELAPSED(millis(), CAN_next_error_message_time)) { + + BUZZ(1, SOUND_ERROR); + + if (CAN_toolhead_error) { + SERIAL_ECHOLNPGM(">>> CAN Error reported by toolhead"); + CAN_toolhead_error = false; // Reset, but will be repeated by the toolhead + } + + if (HAL_FDCAN_error_code) + SERIAL_ECHOLNPGM(">>> HAL CAN Error reported: ", HAL_FDCAN_error_code); + + if (CAN_host_error_code) + SERIAL_ECHOLNPGM(">>> HOST CAN Error Code: ", CAN_host_error_code); + + if (hCAN1.ErrorCode) + SERIAL_ECHOLNPGM(">>> CAN Error Code = ", hCAN1.ErrorCode); + + CAN_next_error_message_time = millis() + CAN_HOST_ERROR_REPEAT_TIME; + } + + if (ELAPSED(millis(), CAN_next_temp_report_time)) { + + CAN_next_temp_report_time = millis() + CAN_HOST_ERROR_REPEAT_TIME; + if (first_E0_error) { // Send error notification + BUZZ(1, SOUND_ERROR); // Warn with sound + SERIAL_ECHOLNPGM("Error: No CAN E0 temp updates"); + } + else // Send only error message + SERIAL_ECHOLNPGM(">>> CAN error: No E0 temp updates"); + + first_E0_error = false; // Warn only once + + #if DISABLED(CAN_DEBUG) // Only kill if not debugging + kill(F("CAN error: No E0 tempeature updates")); + #endif + + if (CAN_toolhead_setup_request) // The toolhead requested the setup configuration + CAN_host_send_setup(true); + } +} + +HAL_StatusTypeDef CAN_host_send_gcode() { // Forward a Marlin Gcode via CAN (uses parser.command_letter, Gcode_no, parser.value_float()) + // Send a Gcode to the toolhead with parameters and values + // Gcode starts with extended frame which can send the Gcode with max 2 parameters and values. + // Extended frames are send to complete all parameters and values (max 2 per extended message). + // 1. Analyze Gcode command + // 2. Ignore gcodes that do not need to be forwarded + // 3. Send parameters and values + // char s[] = "G0 X123.45678 Y124.45678 Z125.45678 E126.45678 F127.45678\n"; + + HAL_StatusTypeDef status = HAL_OK; + + if (parser.command_letter != 'M') // Only forward Mxxx Gcode to toolhead + return HAL_OK; + + uint32_t Gcode_type = CAN_ID_GCODE_TYPE_M; // M-code, fixed for now + uint32_t Gcode_no = parser.codenum & CAN_ID_GCODE_NUMBER_MASK; + + if (Gcode_no == 109) // Convert M109(Hotend wait) to M104 (no wait) to keep the toolhead responsive + Gcode_no = 104; + + if ((Gcode_no == 501) || (Gcode_no == 502)) // M501=Restore settings, M502=Factory defaults + CAN_toolhead_setup_request = true; // Also update settings for the toolhead + + if ((Gcode_no != 104) && // Set hotend target temp + (Gcode_no != 106) && // Set cooling fan speed + (Gcode_no != 107) && // Cooling fan off + (Gcode_no != 115) && // Firmware info (testing) + (Gcode_no != 119) && // Endstop status (testing) + (Gcode_no != 150) && // Set NeoPixel values + //(Gcode_no != 108) && // Break and Continue + (Gcode_no != 280) && // Servo position + (Gcode_no != 306) && // MPC settings/tuning + (Gcode_no != 710) && // Control fan PWM + (Gcode_no != 997)) // Reboot + return HAL_OK; // Nothing to do + + uint32_t index; + uint32_t parameter_counter = 0; + char letters[] = "XYZEFPSABCHIJKLOQRTUVW"; // All possible parameters (22), defines scan order, no "D G M N", includes 'T' for autotune (M306 T) + static uint32_t parameters[8] = { 0 }; // Store found parameters, send max 7 parameters (send in pairs, so reserve 8), CodeA=1 (ASCII65), CodeE=5, CodeF=6, CodeX=88-64=24, CodeY=89-64=25, CodeZ=90-64=26 + static float values[8] = { 0 }; // Store found values, send max 7 parameters (send in pairs, so reserve 8) + + uint8_t CAN_tx_buffer[8]; // 8 bytes CAN data TX buffer + + /* + switch (parser.command_letter) // Filter/adjust Gcodes + { + case 'G': Gcode_type = CAN_ID_GCODE_TYPE_G; + switch (Gcode_no) + { + case 12: break; // No Nozzle cleaning support needed on toolhead + case 29: case 34: return HAL_OK; // No bedleveling/Z-syncing on toolhead + break; + } + break; + + case 'M': Gcode_type = CAN_ID_GCODE_TYPE_M; + switch (Gcode_no) + { // Save Prog mem: M112, M48, M85, M105, M114, M155, M500, M501, M502, M503, M226, M422 + case 109: Gcode_no = 104; break; // Replace M109 with M104 + case 112: Gcode_no = 104; break; // Don't shutdown board, should stop heating with "M104" + + case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 524: case 540: case 928: + case 27: case 28: case 29: case 30: case 32: case 33: case 34: // No SD file commands + case 43: // No pin debug + case 48: // No repeatability test + case 85: // No inactivity shutdown + case 100: // No show free memory support + case 108: // Break and Continue + case 105: // No temperature reporting + case 114: // Don't report position + case 117: case 118: case 119: // Don't send strings + case 140: case 190: // Ignore bed temp commands + case 150: // Set NeoPixel values + case 154: // No auto position reporting + case 155: // No tempeature reporting + case 226: // Wait for pin state + case 240: // No camera support + case 250: // No LCD contrast support + case 260: case 261: // No I2C on toolhead + case 280: // Don't send servo angle, done via Servo.cpp already + case 290: // No baby stepping + case 300: // No tones + // case 303: // No PID autotune (done on TOOLHEAD) + case 304: // No bed PID settings + // case 306: // MPC autotune (done on TOOLHEAD) + case 350: case 351: // No live microstepping adjustment + case 380: case 381: // No solenoid support + case 401: case 402: // No probe deploy/stow, done via M280 servo angles + case 412: // Filament runout sensor done by MASTER + case 420: case 421: // No bed leveling state + case 423: // No X Twist Compensation + case 425: // No backlash compensation + case 500: case 501: case 502: case 503: case 504: case 910: // No EEPROM on toolhead, remove M50x commands to save Prog mem + case 510: case 511: case 512: // No locking of the machine + case 605: // No IDEX commands + case 810: case 811: case 812: case 813: case 814: case 815: case 816: case 817: case 818: case 819: + case 851: // + case 871: // No Probe temp config + case 876: // No handle prompt response + case 913: // No Set Hybrid Threshold Speed + case 914: // No TMC Bump Sensitivity + case 997: // No remote reset + case 998: // No ESP3D reset + return HAL_OK; // NO CAN MESSAGE + } + break; + + case 'T': Gcode_type = CAN_ID_GCODE_TYPE_T; + switch (Gcode_no) + { + case 0: case 1: + break; + } + break; + + case 'D': Gcode_type = CAN_ID_GCODE_TYPE_D; + switch (Gcode_no) + { + case 0: case 1: + break; + } + break; + default: return HAL_OK; // Invalid command, nothing to do + } + */ + + #if ENABLED(CAN_DEBUG) + SERIAL_ECHOPGM(">>> CAN Gcode to toolhead: "); + SERIAL_CHAR(parser.command_letter); + SERIAL_ECHO(Gcode_no); + #endif + + if (strlen(parser.command_ptr) > 4) // "M107\0", Only scan for parameters if the string is long enough + for (index = 0; index < sizeof(letters); index++) { // Scan parameters + if (parser.seen(letters[index])) { + parameters[parameter_counter] = (letters[index] - 64) & CAN_ID_PARAMETER_LETTER_MASK; // Store parameter letter, A=1, B=2... + + #if ENABLED(CAN_DEBUG) + SERIAL_CHAR(' ', letters[index]); + #endif + + if (parser.has_value()) { // Check if there is a value + values[parameter_counter++] = parser.value_float(); + + #if ENABLED(CAN_DEBUG) + if (values[parameter_counter - 1] == int(values[parameter_counter - 1])) + SERIAL_ECHO(i16tostr3left(values[parameter_counter - 1])); // Integer value + else + SERIAL_ECHO(p_float_t(values[parameter_counter - 1], 4)); // Float with 4 digits + #endif + } + else // No value for parameter + values[parameter_counter++] = NAN; // Not A Number, indicates no parameter value is present + } + + if (parameter_counter == 8) { // Max is 7 parameters + CAN_host_error_code |= CAN_ERROR_HOST_INVALID_GCODE; + parameter_counter--; + SERIAL_ECHOLNPGM("\nError: TOO MANY PARAMETERS (> 7): ", parser.command_ptr); + BUZZ(1, SOUND_ERROR); + break; + } + } + + #if ENABLED(CAN_DEBUG) + SERIAL_EOL(); + #endif + + parameters[parameter_counter] = 0; // Set next parameter to 0 (0=no parameter), send in pairs + index = 0; + float * fp = (float *)CAN_tx_buffer; // Points to TX buffer + + TxHeader.IdType = FDCAN_EXTENDED_ID; // Start Gcode with Extended ID, then send Standard ID messages messages if there are more than 2 parameters + + uint32_t deadline = millis() + CAN_HOST_MAX_WAIT_TIME; // Record message send start time + do { + TxHeader.DataLength = MIN(8, (parameter_counter - index) << 2) << FDCAN_HOST_DATALENGTH_OFFSET; // 4 bytes --> only 1 parameter, 8 bytes --> 2 parameters + + if (TxHeader.IdType == FDCAN_EXTENDED_ID) + TxHeader.Identifier = CAN_set_extended_id(Gcode_type, Gcode_no, parameters[index], parameters[index + 1], parameter_counter); + else { + CAN_host_FIFO_toggle_bit = !CAN_host_FIFO_toggle_bit; + TxHeader.Identifier = (CAN_host_FIFO_toggle_bit ? STDID_FIFO_TOGGLE_BIT : 0) | // Toggle bit + (parameters[index ] << CAN_ID_PARAMETER1_BIT_POS) | // Parameter 1 + (parameters[index + 1] << CAN_ID_PARAMETER2_BIT_POS); // Parameter 2 + } + + *fp++ = values[index++]; // Copy first parameter value to data, move pointer to next 4 bytes + *fp-- = values[index++]; // Copy 2nd parameter value to data, move pointer to beginning of data array for next round + + while ((HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1) == 0) && PENDING(millis(), deadline)) { /* BLOCKING! Wait for empty TX Buffer */ } + + if (HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1)) + status = HAL_FDCAN_AddMessageToTxFifoQ(&hCAN1, &TxHeader, CAN_tx_buffer); // Queue CAN message + else + CAN_host_error_code |= CAN_ERROR_HOST_TX_MSG_DROPPED; + + if (status != HAL_OK) return status; + + TxHeader.IdType = FDCAN_STANDARD_ID; // All following messages have standard ID for parameter values, 11 bits identifier + + } while (index < parameter_counter); + + return status; + +} // CAN_host_send_gcode + +void CAN_host_send_position() { // Send the X, Y, Z and E position to the TOOLHEAD + CAN_host_send_gcode_2params('G', 92, 'X', current_position.x, 'Y', current_position.y); // M92 X Y + CAN_host_send_gcode_2params('G', 92, 'Z', current_position.z, 'E', current_position.e); // M92 E Z +} + +// Enable a GPIO clock based on the GPIOx address for STM32H7 +void gpio_clock_enable(GPIO_TypeDef *regs) +{ + uint32_t pos = ((uint32_t)regs - D3_AHB1PERIPH_BASE) >> 10; + RCC->AHB4ENR |= (1 << pos); + RCC->AHB4ENR; +} + +// TODO: SETUP HARDWARE BASED ON CAN_RD_PIN, CAN_TD_PIN PINS +void HAL_FDCAN_MspInit(FDCAN_HandleTypeDef* canHandle) { // Called by HAL_FDCAN_Init + + __HAL_RCC_FDCAN_CLK_ENABLE(); // Enable FDCAN1/2 clock + + // Use some macros to find the required setup info based on the provided CAN pins + uint32_t _CAN_RD_pin = digitalPinToPinName(CAN_RD_PIN); + uint32_t _CAN_TD_pin = digitalPinToPinName(CAN_TD_PIN); + uint32_t _CAN_RD_function = pinmap_find_function(digitalPinToPinName(CAN_RD_PIN), PinMap_CAN_RD); + uint32_t _CAN_TD_function = pinmap_find_function(digitalPinToPinName(CAN_TD_PIN), PinMap_CAN_TD); + + // Enable the GPIOx device related to the CAN_RD_pin + gpio_clock_enable(get_GPIO_Port(STM_PORT(_CAN_RD_pin))); + + GPIO_InitTypeDef GPIO_InitStruct = { 0 }; + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; + GPIO_InitStruct.Pull = STM_PIN_PUPD(_CAN_RD_function); + + // SETUP CAN_RD_PIN + GPIO_InitStruct.Pin = STM_GPIO_PIN(STM_PIN(_CAN_RD_pin)); + GPIO_InitStruct.Alternate = STM_PIN_AFNUM(_CAN_RD_function); + HAL_GPIO_Init(get_GPIO_Port(STM_PORT(_CAN_RD_pin)), &GPIO_InitStruct); + + // SETUP CAN_TD_PIN (Separated for flexibility, might not be needed) + // Enable the GPIOx device related to the CAN_TD_pin + gpio_clock_enable(get_GPIO_Port(STM_PORT(_CAN_TD_pin))); + GPIO_InitStruct.Pin = STM_GPIO_PIN(STM_PIN(_CAN_TD_pin)); + GPIO_InitStruct.Alternate = STM_PIN_AFNUM(_CAN_TD_function); + HAL_GPIO_Init(get_GPIO_Port(STM_PORT(_CAN_TD_pin)), &GPIO_InitStruct); + + // Enabled the FDCAN interrupt handler for FDCAN1 or FDCAN2 + HAL_NVIC_SetPriority((canHandle->Instance == FDCAN1) ? FDCAN1_IT0_IRQn : FDCAN2_IT0_IRQn, 1, 1); // Set FDCAN interrupt priority + HAL_NVIC_EnableIRQ( (canHandle->Instance == FDCAN1) ? FDCAN1_IT0_IRQn : FDCAN2_IT0_IRQn); // Enable FDCAN interrupt handler line 0 (default) +} + +// Calculate the CAN sample timing, seg1 and seg2, sjw = 1 (no baudrate switching) +// seg1 range: 2-256, seg2 range: 1-128, SJW = 1, so minimum is 4 clocks per bit +int FDCAN_calculate_segments(uint32_t *seg1, uint32_t *seg2) { + + uint32_t CAN_clock = HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN); + + float clocks_per_bit = CAN_clock / CAN_BAUDRATE; // Clocks per bit must be a whole number + + if ((clocks_per_bit != int(clocks_per_bit)) || (clocks_per_bit < 4)) // Minimal 4 clocks per bit (1+2+1) + return -1; // Baudrate is not possible + + *seg2 = (clocks_per_bit / 8) + 0.5; // Preferred sample point at 87.5% (7/8) + *seg1 = uint32_t(clocks_per_bit) - *seg2 - 1; // SJW = 1; + + return HAL_OK; +} + +HAL_StatusTypeDef CAN_host_stop() { + return HAL_FDCAN_Stop(&hCAN1); +} + +HAL_StatusTypeDef CAN_host_start() { + + HAL_StatusTypeDef status = HAL_OK; + +// The FDCAN clock source must to be set early because the sample timing depends on it + __HAL_RCC_FDCAN_CONFIG(RCC_FDCANCLKSOURCE_HSE); // 25MHz, select external crystal clock oscillator +// __HAL_RCC_FDCAN_CONFIG(RCC_FDCANCLKSOURCE_PLL); // 55MHz, select PLL +// __HAL_RCC_FDCAN_CONFIG(RCC_FDCANCLKSOURCE_PLL2); // 80MHz, select PLL2 + + // Initialize TxHeader with constant values + TxHeader.FDFormat = FDCAN_CLASSIC_CAN; // FDCAN_CLASSIC_CAN / FDCAN_FD_CAN + TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; // FDCAN_NO_TX_EVENTS / FDCAN_STORE_TX_EVENTS + TxHeader.MessageMarker = 0; // 0-0xFF for tracking messages in FIFO buffer + TxHeader.ErrorStateIndicator = FDCAN_ESI_ACTIVE; // FDCAN_ESI_PASSIVE / FDCAN_ESI_ACTIVE (ACTIVE will notify transmission errors) + TxHeader.TxFrameType = FDCAN_DATA_FRAME; // FDCAN_DATA_FRAME / FDCAN_REMOTE_FRAME + TxHeader.IdType = FDCAN_STANDARD_ID; // FDCAN_STANDARD_ID / FDCAN_EXTENDED_ID + TxHeader.BitRateSwitch = FDCAN_BRS_OFF; // FDCAN_BRS_OFF / FDCAN_BRS_ON + TxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; // FDCAN_NO_TX_EVENTS / FDCAN_STORE_TX_EVENTS (Do not store TX events) + + hCAN1.Instance = FDCAN1; // The FDCAN device to use + hCAN1.Init.FrameFormat = FDCAN_FRAME_CLASSIC; // FDCAN_FRAME_CLASSIC / FDCAN_FRAME_FD_BRS / FDCAN_FRAME_FD_NO_BRS (Bit Rate Switching) + hCAN1.Init.Mode = FDCAN_MODE_NORMAL; // FDCAN_MODE_NORMAL / FDCAN_MODE_EXTERNAL_LOOPBACK / FDCAN_MODE_INTERNAL_LOOPBACK / FDCAN_MODE_BUS_MONITORING / FDCAN_MODE_RESTRICTED_OPERATION + hCAN1.Init.AutoRetransmission = DISABLE; // Auto Retransmission of message if error occured, warning: can cause lockup + hCAN1.Init.TransmitPause = DISABLE; // Transmit Pause to allow for other device to send message + hCAN1.Init.ProtocolException = DISABLE; // ProtocolException handling + + // FDCAN baud rate = FDCAN Clock / Prescaler / (SJW + TSG1 + TSG2) + // Sample-Point from 50 to 90% (87.5 % is the preferred value used by CANopen and DeviceNet, 75% is the ARINC 825 default) + // STM32H7xx @ 550MHz, FDCAN clock: HSE=25MHz, PLL=55Mhz, PLL2=80MHz + // Baud rate = 25M / 1 / 1 / (1 + 21 + 3) = 1M baud (Sample point = 22/25 = 88%) + // Baud rate = 25M / 1 / 2 / (1 + 21 + 3) = 500k baud + // HSE 25MHz: ( 1 1 21 3) --> 1M baud; ( 2 1 21 3) --> 500K baud; ( 4 1 21 3) --> 250K baud; + // PLL 55MHz: (11 1 3 1) --> 1M baud; ( 22 1 3 1) --> 500K baud; (44 1 3 1) --> 250K baud; + // PLL2 80MHz: ( 1 1 69 10) ( 2 1 34 5) ( 4 1 16 3) --> 1M baud + // PLL2 80MHz: ( 2 1 69 10) ( 4 1 34 5) ( 8 1 16 3) --> 500K baud + // PLL2 80MHz: ( 4 1 69 10) ( 8 1 34 5) (16 1 16 3) --> 250K baud + + uint32_t seg1, seg2; + if (FDCAN_calculate_segments(&seg1, &seg2) != HAL_OK) { + SERIAL_ECHOLNPGM("Impossible CAN baudrate, check CAN clock and baudrate"); + return HAL_ERROR; + } + + hCAN1.Init.NominalPrescaler = 1; // Arbitration/data clock prescaler/divider (1-512) + hCAN1.Init.NominalSyncJumpWidth = 1; // Arbitration/data Sync Jump Width (1-128 SJW), should be 1 + hCAN1.Init.NominalTimeSeg1 = seg1; // Arbitration/data period 1 (2-256) // 21 + hCAN1.Init.NominalTimeSeg2 = seg2; // Arbitration/data period 2 (2-128) // 3 + +/* No bitrate switching, not used: + hCAN1.Init.DataPrescaler = 1; // Arbitration/data clock prescaler/divider (1-32) + hCAN1.Init.DataSyncJumpWidth = 1; // Arbitration/data Sync Jump Width (1-16 SJW), should be 1 + hCAN1.Init.DataTimeSeg1 = 21; // Arbitration/data period 1 (2-32) + hCAN1.Init.DataTimeSeg2 = 3; // Arbitration/data period 2 (1-16) +*/ + hCAN1.Init.MessageRAMOffset = 0; // Only using 1 FDCAN device, so offset is 0. FDCAN_MESSAGE_RAM_SIZE: 2560 Words, 10KBytes + hCAN1.Init.TxEventsNbr = 0; // 0-32 + hCAN1.Init.TxBuffersNbr = 0; // 0-32 + hCAN1.Init.TxFifoQueueElmtsNbr = FDCAN_HOST_TX_FIFO_DEPTH; // 0-32 + hCAN1.Init.TxElmtSize = FDCAN_DATA_BYTES_8; + + hCAN1.Init.RxBufferSize = FDCAN_DATA_BYTES_8; // FDCAN_data_field_size + hCAN1.Init.RxBuffersNbr = 0; // 0-64 + hCAN1.Init.RxFifo0ElmtsNbr = FDCAN_HOST_RX_FIFO_DEPTH; // 0-64 + hCAN1.Init.RxFifo0ElmtSize = FDCAN_DATA_BYTES_8; + hCAN1.Init.RxFifo1ElmtsNbr = FDCAN_HOST_RX_FIFO_DEPTH; // 0-64 + hCAN1.Init.RxFifo1ElmtSize = FDCAN_DATA_BYTES_8; + + hCAN1.Init.StdFiltersNbr = 2; // Number of standard frame filters + hCAN1.Init.ExtFiltersNbr = 2; // Number of extended frame filters + hCAN1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; // Queue mode: FDCAN_TX_FIFO_OPERATION / FDCAN_TX_QUEUE_OPERATION + + status = HAL_FDCAN_Init(&hCAN1); // Calls HAL_FDCAN_MspInit + if (status != HAL_OK) return status; + + // FDCAN Filter configuration, toggle reception between FIFO0 and FIFO1 (not really needed for this FDCAN implementation) + FDCAN_FilterTypeDef sFilterConfig; // Configure RX message filter + + sFilterConfig.IdType = FDCAN_EXTENDED_ID; // Filter extended ID messages to FIFO1 if higest ID bit is set + sFilterConfig.FilterIndex = 0; // Exteneded filter ID 0 + sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1; // Filter to FIFO1 + sFilterConfig.FilterID1 = 0x10000000; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + sFilterConfig.FilterID2 = 0x10000000; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + +//sFilterConfig.IdType = FDCAN_EXTENDED_ID; // Filter all remaining extended ID messages to FIFO0 + sFilterConfig.FilterIndex = 1; // Exteneded filter ID 1 +//sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; // Remaining messages go to FIFO0 + sFilterConfig.FilterID1 = 0; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + sFilterConfig.FilterID2 = 0; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + + sFilterConfig.IdType = FDCAN_STANDARD_ID; // Filter to FIFO1 if higest standard ID bit is set + sFilterConfig.FilterIndex = 0; // Standard filter ID 0 + sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; + sFilterConfig.FilterID1 = 0b10000000000; // Range: 0 - 0x7FF, 0=don't care + sFilterConfig.FilterID2 = 0b10000000000; // Range: 0 - 0x7FF, 0=don't care + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + +//sFilterConfig.IdType = FDCAN_STANDARD_ID; // Filter all remaining standard ID messages to FIFO0 + sFilterConfig.FilterIndex = 1; // Standard filter ID 1 +//sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1; // Remaining to FIFO0 + sFilterConfig.FilterID1 = 0; // Range: 0 - 0x7FF, 0=don't care + sFilterConfig.FilterID2 = 0; // Range: 0 - 0x7FF, 0=don't care + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + + // status = HAL_FDCAN_ConfigGlobalFilter(&hfdcan, FDCAN_REJECT, FDCAN_REJECT, FDCAN_REJECT_REMOTE, FDCAN_REJECT_REMOTE); + //if (status != HAL_OK) return status; + + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0); // Calls HAL_FDCAN_RxFifo0MsgPendingCallback + if (status != HAL_OK) return status; + + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO1_NEW_MESSAGE, 0); // Calls HAL_FDCAN_RxFifo1MsgPendingCallback + if (status != HAL_OK) return status; + + HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO0_MESSAGE_LOST, 0); // Calls HAL_FDCAN_ErrorCallback + if (status != HAL_OK) return status; + + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO1_MESSAGE_LOST, 0); // Calls HAL_FDCAN_ErrorCallback + if (status != HAL_OK) return status; + + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_ERROR_WARNING, 0); // Calls HAL_FDCAN_ErrorCallback + if (status != HAL_OK) return status; + + status = HAL_FDCAN_Start(&hCAN1); // Start the FDCAN device + if (status != HAL_OK) return status; + + #if ENABLED(CAN_DEBUG) + SERIAL_ECHOLNPGM("Voltage Scaling: VOS", (PWR->CSR1 & PWR_CSR1_ACTVOS_Msk) >> PWR_CSR1_ACTVOS_Pos); + SERIAL_ECHOLNPGM("FDCAN Peripheral Clock : ", HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN) / 1000000, "MHz"); + SERIAL_ECHOLNPGM("FDCAN Timing. Prescaler: ", hCAN1.Init.NominalPrescaler, " SJW: ", hCAN1.Init.NominalSyncJumpWidth, " SEG1: ", hCAN1.Init.NominalTimeSeg1, " SEG2: ", hCAN1.Init.NominalTimeSeg2); + uint32_t baudrate = HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN) / hCAN1.Init.NominalPrescaler / (hCAN1.Init.NominalSyncJumpWidth + hCAN1.Init.NominalTimeSeg1 + hCAN1.Init.NominalTimeSeg2); + SERIAL_ECHOLNPGM("FDCAN BAUDRATE: ", baudrate); + + if (baudrate != CAN_BAUDRATE) { + SERIAL_ECHOLNPGM(">>> Host ", CAN_ERROR_MSG_INVALID_BAUDRATE, ": ", baudrate, " CAN_BAUDRATE=", CAN_BAUDRATE); + CAN_host_error_code |= CAN_ERROR_HOST_INVALID_BAUDRATE; + } + #endif + + #ifdef CAN_LED_PIN + pinMode(CAN_LED_PIN, OUTPUT); + #endif + + status = CAN_host_send_gcode_2params('M', 997, 0, 0, 0, 0); // M997, reset toolhead at host startup + + return status; +} // CAN_host_start() + +void FDCAN_host_read_message(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo) { // ISR! FIFO 0/1 message interrupt handler + + FDCAN_RxHeaderTypeDef RxHeader; + uint8_t CAN_RX_buffer_FIFO[8]; // CAN message buffer + + if (HAL_FDCAN_GetRxFifoFillLevel(hfdcan, RxFifo) && + (HAL_FDCAN_GetRxMessage(hfdcan, RxFifo, &RxHeader, CAN_RX_buffer_FIFO) == HAL_OK)) { // Get message from CAN_RX_FIFO0 + + if ((RxHeader.Identifier & CAN_ID_IO_MASK) != CAN_io_state) { // First handle time critical virtual IO update + CAN_io_state = (RxHeader.Identifier & CAN_ID_IO_MASK); + endstops.update(); + } + + if (RxHeader.Identifier & CAN_ID_STRING_MESSAGE_BIT_MASK) { // Toolhead sent a string message + char *CAN_RX_p = (char *)CAN_RX_buffer_FIFO; + for (uint32_t i = 0; i < (RxHeader.DataLength >> FDCAN_HOST_DATALENGTH_OFFSET); i++) + string_message.append(CAN_RX_p[i]); // Copy message to global buffer + + if (CAN_RX_p[(RxHeader.DataLength >> FDCAN_HOST_DATALENGTH_OFFSET) - 1] == '\n') + string_message_complete = true; // String is complete, idle task can show the string + } + else if (RxHeader.DataLength == FDCAN_DLC_BYTES_4) { // Only 1 record, so it's a temperature update (DLC = Data Length Code == 4 bytes) + float *fp = (float *)CAN_RX_buffer_FIFO; + thermalManager.temp_hotend[0].celsius = *fp; // Set E0 hotend temperature from received message + CAN_next_temp_report_time = millis() + CAN_HOST_E0_TEMP_UPDATE_WATCHDOG_TIME; // A temp update must be received within this window + first_E0_error = true; // Reset error status + } + + if (RxHeader.Identifier & CAN_ID_REQUEST_TIME_SYNC_BIT_MASK) { // Toolhead signals request for time stamp + time_sync_request_time = micros(); // Record the time sync request receive time + CAN_time_sync_request = true; + } + + CAN_toolhead_setup_request = (RxHeader.Identifier & CAN_ID_REQUEST_SETUP_BIT_MASK) > 0; // Toolhead requests setup configuration + + CAN_toolhead_error = (RxHeader.Identifier & CAN_ID_ERROR_BIT_MASK) > 0; // Toolhead signals an error + } +} + +void FDCAN1_IT0_IRQHandler() { // ISR! FDCAN line 0 interrupt handler (overrides weak function) + + #ifdef FDCAN_LED_PIN + pinMode(FDCAN_LED_PIN, OUTPUT); + TOGGLE(FDCAN_LED_PIN); + #endif + + HAL_FDCAN_IRQHandler(&hCAN1); // Forward the interrupt call to the FDCAN interrupt handler for callbacks + +} + +void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) { // ISR! CAN FIFO0 new message interrupt handler + + if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_MESSAGE_LOST) { // Check the error status first, it might be cleared by reading a message + CAN_host_error_code |= CAN_ERROR_HOST_RX_FIFO_OVERFLOW; + __HAL_FDCAN_CLEAR_FLAG(hfdcan, FDCAN_IT_RX_FIFO0_MESSAGE_LOST); // Clear interrupt flag + } + + if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) + FDCAN_host_read_message(hfdcan, FDCAN_RX_FIFO0); // Forward call + +} + +void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs) { // ISR! CAN FIFO1 new message interrupt handler + + if (RxFifo1ITs & FDCAN_IT_RX_FIFO1_MESSAGE_LOST) { // Check the error status first, it might be cleared by reading a message + CAN_host_error_code |= CAN_ERROR_HOST_RX_FIFO_OVERFLOW; + __HAL_FDCAN_CLEAR_FLAG(hfdcan, FDCAN_IT_RX_FIFO1_MESSAGE_LOST); // Clear interrupt flag + } + + if ((RxFifo1ITs & FDCAN_IT_RX_FIFO1_NEW_MESSAGE)) + FDCAN_host_read_message(hfdcan, FDCAN_RX_FIFO1); // Forward call +} + +void HAL_FDCAN_ErrorStatusCallback(FDCAN_HandleTypeDef *hfdcan, uint32_t ErrorStatusITs) { // ISR! + + // FDCAN_IR_EP | FDCAN_IR_EW | FDCAN_IR_BO ERROR PASSIVE, WARNINGS, BUS OFF + HAL_FDCAN_error_code = hfdcan->ErrorCode; // Store the received FDCAN error code +} + +void HAL_FDCAN_ErrorCallback(FDCAN_HandleTypeDef *hfdcan) { // ISR! FDCAN error interrupt handler +// FDCAN_IR_ELO | FDCAN_IR_WDI | FDCAN_IR_PEA | FDCAN_IR_PED | FDCAN_IR_ARA LOGGING OVERFLOW, WATCHDOG INTERRUPT, PROTOCOL ERRORS, ACCESS RESERVED AREA + HAL_FDCAN_error_code = hfdcan->ErrorCode; // Store the received FDCAN error code +} + +#endif // CAN_HOST && STM32H7xx diff --git a/Marlin/src/HAL/STM32/FDCAN_toolhead.cpp b/Marlin/src/HAL/STM32/FDCAN_toolhead.cpp new file mode 100644 index 000000000000..e1807f5a7915 --- /dev/null +++ b/Marlin/src/HAL/STM32/FDCAN_toolhead.cpp @@ -0,0 +1,743 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +/** + * Contributor Notes: + * =============== + * In \Users\\.platformio\packages\framework-arduinoststm32@4.20600.231001\libraries\SrcWrapper\src\HardwareTimer.cpp + * Add "__weak" in front of "void TIM16_IRQHandler(void)" to be able to override the function and redirect the interrupt + */ + +#include "../../inc/MarlinConfigPre.h" + +#if ENABLED(CAN_TOOLHEAD) +// TODO: +// 1. Convert into class +// 2. Pickup correct CAN device from CAN_RX_PIN/CAN_TX_PIN +// 3. Calculate baudrate and data sampling timing from clock frequencies defined by CAN_BAUDRATE + +#include "../platforms.h" +#include "../../gcode/gcode.h" +#include "../../gcode/parser.h" +#include "../../gcode/queue.h" +#include "../../module/temperature.h" +#include "../../libs/numtostr.h" +#include "../../inc/MarlinConfig.h" +#include "../../feature/controllerfan.h" +#include "../../core/serial.h" + +#include "../shared/CAN.h" + +extern "C" void TIM16_IRQHandler(void); // Override weak functions +extern "C" void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs); +extern "C" void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs); + +#ifndef CAN_LED_PIN + #define CAN_LED_PIN LED_PIN +#endif + +#define CAN_DATALENGTH_OFFSET 16 // Used for CAN TxHeader DataLength record + +#ifndef CAN_BAUDRATE + #define CAN_BAUDRATE 1000000LU +#endif + +#ifndef CAN_RD_PIN + #define CAN_RD_PIN PB0 +#endif +#ifndef CAN_TD_PIN + #define CAN_TD_PIN PB1 +#endif + +#define FDCAN_RX_FIFO0_MASK (FDCAN_IR_RF0L | FDCAN_IR_RF0F | FDCAN_IR_RF0N) // FIFO0: Message lost | FIFO full | New message +#define FDCAN_RX_FIFO1_MASK (FDCAN_IR_RF1L | FDCAN_IR_RF1F | FDCAN_IR_RF1N) // FIFO1: Message lost | FIFO full | New message +#define FDCAN_RX_FIFO_MASK (FDCAN_RX_FIFO0_MASK | FDCAN_RX_FIFO1_MASK) // FIFO : Message lost | FIFO full | New message +#define HAL_TIM_FLAG_ALL_MASK (TIM_FLAG_CC1 | TIM_FLAG_CC2 | TIM_FLAG_CC3 | TIM_FLAG_CC4 | TIM_FLAG_UPDATE | TIM_FLAG_BREAK | TIM_FLAG_BREAK2 | TIM_FLAG_TRIGGER | TIM_FLAG_COM) +#define M_GCODE(A) ((CAN_ID_GCODE_TYPE_M << (CAN_ID_GCODE_TYPE_BIT_POS - CAN_ID_GCODE_NUMBER_BIT_POS)) + A) + +#define CAN_FIFO_DEPTH 3 // 3 FIFO buffers per FIFO on STM32G0xx +#define CAN_MAX_WAIT_TIME 25 // Amount of ms to wait for resources (TX buffers/Marlin command buffers) +#define CAN_TIMESTAMP_NO 7777 // M7777 is used to handle timesync messages +#define CAN_TOOLHEAD_CONFIGURATION_COMPLETE 7778 // Signal the configuration is complete +#define CAN_TEMPERATURE_REPORT_INTERVAL 950 // Temperature report interval in ms (faster than once a second) +#define CAN_MAX_STRING_MESSAGE_LENGTH 128 // Max string length to send to the host +#define CAN_EXTENDED_ID_MARKER_MASK (1 << 31) // Extended ID marker in CAN_BUFFER identifier +#define CAN_QUEUE_DEPTH 16 // CAN queue depth + +FDCAN_HandleTypeDef hCAN1; // The global CAN handle + +struct CAN_MSG_BUFFER { // Struct to hold a CAN message + uint32_t identifier; + float data[2]; + uint32_t receive_time; // Used for time sync +}; + +SString CAN_string_buffer; // String buffer to hold outgoing string messages +char gcode_type[4] = { 'D', 'G', 'M', 'T' }; // The 4 Gcode types +volatile bool CAN_toolhead_not_configured = true; // Track if the host sent the required toolhead configuration +uint32_t CAN_toolhead_error_code = 0; // Signals an CAN error occured, report to host +uint32_t CAN_previous_error_code = 0; // Remember last error code so changes can be detected +uint32_t CAN_next_temp_report_time = 0; // The next temperature report time, delay on startup +uint32_t CAN_send_next_string_part_time = 0; // Send the next part of a string message +volatile bool CAN_FIFO_toggle_bit = 0; // FIFO toggle bit for receiver filtering to FIFO0 and FIFO1 +bool CAN_request_time_sync = false; // Request a timestamp for NTP time sync + +int CAN_NTP_time_offset = 0; // Time offset in micro seconds in relation to the host time +volatile uint32_t NTP[5] = { 0 }; // NTP style timestamps for time sync +float CAN_NTP_clock_drift = 0; // Time sync calculated clock drift between host and toolhead + +uint32_t CAN_next_led_flash_time = 0; // Error LED flashing +uint32_t CAN_next_error_message_time = 0; // Controls error message repeat frequency + +CAN_MSG_BUFFER CAN_QUEUE[CAN_QUEUE_DEPTH] = { 0 }; // Circular CAN message queue +volatile uint32_t CAN_queue_head = 0; // Queue head index +volatile uint32_t CAN_queue_tail = 0; // Queue tail index + +// Function to calculate the CAN message ID holding various bits of information +uint32_t CAN_get_virtual_IO(bool tempUpdate) { + + CAN_FIFO_toggle_bit = !CAN_FIFO_toggle_bit; // Toggle FIFO bit for receiver filtering to FIFO0 and FIFO1 + + bool configured = tempUpdate && CAN_toolhead_not_configured; // Only request setup during temp updates, not IO interrupts + return ( + + #ifdef Z_MIN_PROBE_PIN + (READ(Z_MIN_PROBE_PIN) << CAN_ID_PROBE_BIT_POS) | // Report probe status + #endif + #ifdef X_MIN_PIN + (READ(X_MIN_PIN) << CAN_ID_X_ENDSTOP_BIT_POS) | // Report X-min status + #endif + #ifdef Y_MIN_PIN + (READ(Y_MIN_PIN) << CAN_ID_Y_ENDSTOP_BIT_POS) | // Report Y-min status + #endif + #ifdef Z_MIN_PIN + (READ(Z_MIN_PIN) << CAN_ID_Z_ENDSTOP_BIT_POS) | // Report Z-min status + #endif + #ifdef FILAMENT_RUNOUT_PIN + (READ(FILAMENT_RUNOUT_PIN) << CAN_ID_FILAMENT_BIT_POS) | // Report filament detector status + #endif + + (CAN_FIFO_toggle_bit ? STDID_FIFO_TOGGLE_BIT : 0) | // Add FIFO toggle bit + (configured << CAN_ID_REQUEST_SETUP_BIT_POS) | // Request toolhead setup configuration + ((!!CAN_toolhead_error_code) << CAN_ID_ERROR_BIT_POS) // Report error (if any) + ); +} + +// Process a received message (offline in the idle handler) +void process_can_queue() { + static SString CAN_gcode_buffer; + static uint32_t parameter_counter = 0; + + uint32_t identifier = CAN_QUEUE[CAN_queue_tail].identifier; + bool enqueue = true; + + // Receiving new Gcode + if (identifier & CAN_EXTENDED_ID_MARKER_MASK) { + + uint32_t gcode = (identifier >> CAN_ID_GCODE_NUMBER_BIT_POS) & CAN_ID_GCODE_MASK; + + // M997, restart the toolhead unconditionally + if (gcode == M_GCODE(997)) { + SERIAL_ECHOLNPGM("\n>>> Host requested a toolhead restart (M997)..."); + delay(25); + flashFirmware(0); + while (1); + } + + // The host indicatds that the configuration is complete + if (gcode == M_GCODE(CAN_TOOLHEAD_CONFIGURATION_COMPLETE)) { + CAN_toolhead_not_configured = false; + MString<60> buffer("FIRMWARE ", __DATE__, " ", __TIME__, " Thermistor=", TEMP_SENSOR_0); + CAN_toolhead_send_string(buffer); + enqueue = false; + } + + // M115 triggers the time sync process + if (gcode == M_GCODE(115)) // TESTING... + CAN_request_time_sync = true; // Send time sync request in idle task and wait for response + + // M, time sync response message + if (gcode == M_GCODE(CAN_TIMESTAMP_NO)) { + // Data contains 2 timestamps in uint32_t format + uint32_t *uint32p = (uint32_t*)CAN_QUEUE[CAN_queue_tail].data; + NTP[1] = *uint32p++; + NTP[2] = *uint32p; + NTP[3] = CAN_QUEUE[CAN_queue_tail].receive_time; // Record time sync response message receive time + + enqueue = false; // Timestamps were stored, the idle process will handle the rest + } + + // New Gcode started, so previous Gcode must be complete (all parameters received) + if (parameter_counter) + CAN_toolhead_error_code |= CAN_ERROR_TOOLHEAD_INCOMPLETE_GCODE_RECEIVED; + + parameter_counter = (identifier >> CAN_ID_PARAMETER_COUNT_BIT_POS) & CAN_ID_PARAMETER_COUNT_MASK; // Get Gcode parameter count + + CAN_gcode_buffer = gcode_type[(identifier >> CAN_ID_GCODE_TYPE_BIT_POS) & CAN_ID_GCODE_TYPE_MASK]; // Add Gcode letter e.g., "G" + CAN_gcode_buffer.append((identifier >> CAN_ID_GCODE_NUMBER_BIT_POS) & CAN_ID_GCODE_NUMBER_MASK); // Add Gcode number e.g., G"92" + } + + uint32_t backupLength = 0; // Backup string length in case we cannot enqueue the Gcode and have to try again + // Add parameters (if present) to the received Gcode + if (parameter_counter && ((identifier >> CAN_ID_PARAMETER1_BIT_POS) & CAN_ID_PARAMETER_LETTER_MASK)) { // Get 1st parameter, make sure it's not empty. + backupLength = CAN_gcode_buffer.length(); // Point to place where parameters are added + CAN_gcode_buffer.append(char((identifier & CAN_ID_PARAMETER_LETTER_MASK) + 64)); // Add Gcode parameter letter, e.g., 'X' + float value = CAN_QUEUE[CAN_queue_tail].data[0]; + if (!isnan(value)) { // No value for parameter if value is "Not A Number" + if (value == int(value)) + CAN_gcode_buffer.append(int(CAN_QUEUE[CAN_queue_tail].data[0])); // Integer value + else + CAN_gcode_buffer.append(p_float_t(CAN_QUEUE[CAN_queue_tail].data[0], 5)); + } + parameter_counter--; + + // Add 2nd parameter if 2 values were provided + if (parameter_counter && ((identifier >> CAN_ID_PARAMETER2_BIT_POS) & CAN_ID_PARAMETER_LETTER_MASK)) { // Get 2nd parameter, make sure it's not empty. + CAN_gcode_buffer.append(char(((identifier >> CAN_ID_PARAMETER2_BIT_POS) & CAN_ID_PARAMETER_LETTER_MASK) + 64)); // Add Gcode parameter letter, e.g., 'X' + if (!isnan(CAN_QUEUE[CAN_queue_tail].data[1])) { // No value for parameter if value is "Not A Number" + if (value == int(value)) + CAN_gcode_buffer.append(int(CAN_QUEUE[CAN_queue_tail].data[1])); // Integer value + else + CAN_gcode_buffer.append(p_float_t(CAN_QUEUE[CAN_queue_tail].data[1], 5)); + } + parameter_counter--; + } + } + + if (!parameter_counter) { // Gcode is complete, including all parameters, process the Gcode + if (enqueue) { + // queue.enqueue_one returns TRUE if the command was queued, FALSE if the Marlin cmd buffer was full + if (queue.enqueue_one(CAN_gcode_buffer)) { // Increase tail only when commands was enqueued + CAN_queue_tail = (CAN_queue_tail + 1) % CAN_QUEUE_DEPTH; + #ifdef CAN_DEBUG + SERIAL_ECHOPGM(";", millis(), " "); CAN_gcode_buffer.echoln(); + #endif + } + else + if (!(identifier & CAN_EXTENDED_ID_MARKER_MASK)) // Standard ID message, so parameters were added to the Gcode + CAN_gcode_buffer.trunc(backupLength); // Cut off the part of the Gcode that was added, so we can process the CAN message again + } + else + CAN_queue_tail = (CAN_queue_tail + 1) % CAN_QUEUE_DEPTH; // Always advance tail + } + else + CAN_queue_tail = (CAN_queue_tail + 1) % CAN_QUEUE_DEPTH; + +} // process_can_queue + +HAL_StatusTypeDef CAN_receive_msg(uint32_t FIFO) { // ISR! Process received CAN message in interrupt handler + + if (((CAN_queue_head + 1) % CAN_QUEUE_DEPTH) != CAN_queue_tail) { // Check if a buffer is available + + FDCAN_RxHeaderTypeDef CAN_rx_header; + CAN_QUEUE[CAN_queue_head].receive_time = micros(); // Save receiver timestamp for time sync + HAL_FDCAN_GetRxMessage(&hCAN1, FIFO, &CAN_rx_header, (uint8_t*)CAN_QUEUE[CAN_queue_head].data); + + if (CAN_rx_header.IdType == FDCAN_EXTENDED_ID) // Mark extended ID message (new Gcode start) + CAN_QUEUE[CAN_queue_head].identifier = CAN_EXTENDED_ID_MARKER_MASK + CAN_rx_header.Identifier; + else + CAN_QUEUE[CAN_queue_head].identifier = CAN_rx_header.Identifier; // Standard ID message (Gcode parameters) + + CAN_queue_head = (CAN_queue_head + 1) % CAN_QUEUE_DEPTH; + } + else + CAN_toolhead_error_code |= CAN_ERROR_TOOLHEAD_RX_FIFO_OVERFLOW; + + return HAL_OK; + +} // CAN_receive_msg + +// NOTE: TIM16 is also used for the stepper! +void TIM16_IRQHandler(void) { // ISR! Combined TIM16 and CAN interrupt handler (override weak function) + + // Check if the timer caused the interrupt + if (TIM16->SR & HAL_TIM_FLAG_ALL_MASK) +// HardwareTimer_Handle[TIMER16_INDEX]->handle.Instance->SR & HAL_TIM_FLAG_ALL_MASK) + HAL_TIM_IRQHandler(&HardwareTimer_Handle[TIMER16_INDEX]->handle); + // Forward call to timer interrupt handler + // + // OR + // + // Forward to Marlin stepper ISR directly? + + // Call the FDCAN interrupt handler + //if (hCAN1.Instance->IR & (FDCAN_IR_RF0N | FDCAN_IR_RF0L | FDCAN_IR_RF1N | FDCAN_IR_RF1L)) + // HAL_FDCAN_IRQHandler(&hCAN1); // Forward call to FDCAN interrupt handler, calls HAL_FDCAN_RxFifo0Callback and HAL_FDCAN_RxFifo1Callback + // + // OR + // + // Call the required callbacks directly, faster but limited error reporting + // New FIFO0 CAN message + if (hCAN1.Instance->IR & FDCAN_IR_RF0N) { + __HAL_FDCAN_CLEAR_FLAG(&hCAN1, FDCAN_IR_RF0N); + CAN_receive_msg(FDCAN_RX_FIFO0); + } + + // New FIFO1 CAN message + if (hCAN1.Instance->IR & FDCAN_IR_RF1N) { + __HAL_FDCAN_CLEAR_FLAG(&hCAN1, FDCAN_IR_RF1N); + CAN_receive_msg(FDCAN_RX_FIFO1); + } + + // Check for lost CAN messages + if (hCAN1.Instance->IR & (FDCAN_IR_RF0L | FDCAN_IR_RF1L)) { + CAN_toolhead_error_code |= CAN_ERROR_TOOLHEAD_RX_FIFO_OVERFLOW; + __HAL_FDCAN_CLEAR_FLAG(&hCAN1, FDCAN_IR_RF0L | FDCAN_IR_RF1L); + } + +} // TIM16_IRQHandler + +void HAL_FDCAN_RxFifo0Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo0ITs) { // ISR! Override "weak" function + + if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_MESSAGE_LOST) + CAN_toolhead_error_code |= CAN_ERROR_TOOLHEAD_RX_FIFO_OVERFLOW; + + if (RxFifo0ITs & FDCAN_IT_RX_FIFO0_NEW_MESSAGE) + CAN_receive_msg(FDCAN_RX_FIFO0); + +} + +void HAL_FDCAN_RxFifo1Callback(FDCAN_HandleTypeDef *hfdcan, uint32_t RxFifo1ITs) { // ISR! Override "weak" function + + if (RxFifo1ITs & FDCAN_IT_RX_FIFO1_MESSAGE_LOST) + __HAL_FDCAN_CLEAR_FLAG(hfdcan, FDCAN_IT_RX_FIFO1_MESSAGE_LOST); // Clear FIFO1 message lost interrupt flag + + if (RxFifo1ITs & FDCAN_IT_RX_FIFO1_NEW_MESSAGE) + CAN_receive_msg(FDCAN_RX_FIFO1); + +} + +// Enable a GPIO clock based on the GPIOx address for STM32G0 +void gpio_clock_enable(GPIO_TypeDef *regs) +{ + // Or use: STM_PORT(CAN_TD_pin) GPIOA=0, GPIOB=1 etc. + uint32_t pos = ((uint32_t)regs - IOPORT_BASE) >> 10; + RCC->IOPENR |= (1 << pos); + RCC->IOPENR; +} + +void HAL_FDCAN_MspInit(FDCAN_HandleTypeDef* fdcanHandle) { // Called automatically by FDCAN_init(), configure GPIO for CAN, enable interrupts + + // STM32G0B1 and STM32G0 C1 only + + // FDCAN2 GPIO Configuration + // PB0 AF3 ------> FDCAN2_RD + // PB1 AF3 ------> FDCAN2_TD + + // CAN1/2 clock enable (one clock for both CAN devices) + __HAL_RCC_FDCAN_CLK_ENABLE(); + + // Use some macros to find the required setup info based on the provided CAN pins + uint32_t _CAN_RD_pin = digitalPinToPinName(CAN_RD_PIN); + uint32_t _CAN_TD_pin = digitalPinToPinName(CAN_TD_PIN); + uint32_t _CAN_RD_function = pinmap_find_function(digitalPinToPinName(CAN_RD_PIN), PinMap_CAN_RD); + uint32_t _CAN_TD_function = pinmap_find_function(digitalPinToPinName(CAN_TD_PIN), PinMap_CAN_TD); + + // Enable the GPIOx device related to the CAN_RD_pin + gpio_clock_enable(get_GPIO_Port(STM_PORT(_CAN_RD_pin))); // PB0 + + GPIO_InitTypeDef GPIO_InitStruct = { 0 }; + GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // High frequency device + GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // Alternate function + GPIO_InitStruct.Pin = STM_GPIO_PIN(STM_PIN(_CAN_RD_pin)) | STM_GPIO_PIN(STM_PIN(_CAN_TD_pin)); + GPIO_InitStruct.Pull = STM_PIN_PUPD(_CAN_RD_function); + GPIO_InitStruct.Alternate = STM_PIN_AFNUM(_CAN_RD_function); + HAL_GPIO_Init(get_GPIO_Port(STM_PORT(_CAN_RD_pin)), &GPIO_InitStruct); + + //NOTE: Separating the pin initialisation causes issues, why? + //Enable the GPIOx device related to the CAN_TD_pin + //gpio_clock_enable(get_GPIO_Port(STM_PORT(_CAN_TD_pin))); + //GPIO_InitStruct.Pin = STM_GPIO_PIN(STM_PIN(_CAN_TD_pin)); + //GPIO_InitStruct.Alternate = STM_PIN_AFNUM(_CAN_TD_function); + //HAL_GPIO_Init(get_GPIO_Port(STM_PORT(_CAN_TD_pin)), &GPIO_InitStruct); + + // Enable CAN interrupts + HAL_NVIC_SetPriority(TIM16_FDCAN_IT0_IRQn, 1, 1); // Set interrupt priority + HAL_NVIC_EnableIRQ(TIM16_FDCAN_IT0_IRQn); // Enable interrupt handler + +} // HAL_FDCAN_MspInit + +// Calculate the CAN sample timing, seg1 and seg2, sjw = 1 (no baudrate switching) +// seg1 range: 2-256, seg2 range: 2-128, SJW = 1, so minimum is 5 clocks per bit +int FDCAN_calculate_segments(uint32_t *seg1, uint32_t *seg2) { + + uint32_t CAN_clock = HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN); + float clocks_per_bit = CAN_clock / CAN_BAUDRATE; // Clocks per bit must be a whole number + + if ((clocks_per_bit != int(clocks_per_bit)) || (clocks_per_bit < 5)) // Minimal 5 clocks per bit (2+2+1) + return -1; // Baudrate is not possible + + *seg2 = (clocks_per_bit / 8) + 0.5; // Preferred sample point at 87.5% (7/8) + *seg1 = uint32_t(clocks_per_bit) - *seg2 - 1; // sjw = 1; + + return HAL_OK; +} + +HAL_StatusTypeDef CAN_toolhead_start() { // Start the CAN device + + // The CAN clock and clock source must to be set first because the sample timing depends on it + RCC_PeriphCLKInitTypeDef PeriphClkInit = { 0 }; + PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_FDCAN; + PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_HSE; // 8MHz external crystal +//PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_PLL; // 64MHz +//PeriphClkInit.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; // 64MHz + HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit); // Select the FDCAN clock source + + HAL_StatusTypeDef status = HAL_OK; + // CAN baud rate = Device Clock Frequency / Clock Divider / Prescaler / (SJW + TSG1 + TSG2) + // Sample-Point from 50 to 90% (87.5 % is the preferred value used by CANopen and DeviceNet, 75% is the ARINC 825 default) + // Baud rate = 64M / 1 / 4 / (1 + 13 + 2) = 1M Bit sample point = (1 + 13) / (1 + 13 + 2) = 87.5% + // Baud rate = 64M / 1 / 8 / (1 + 13 + 2) = 500K Bit sample point = (1 + 13) / (1 + 13 + 2) = 87.5% + // Baud rate = 64M / 1 / 16 / (1 + 13 + 2) = 250K Bit sample point = (1 + 13) / (1 + 13 + 2) = 87.5% + + uint32_t seg1, seg2; + if (FDCAN_calculate_segments(&seg1, &seg2) != HAL_OK) { + SERIAL_ECHOLNPGM("Impossible CAN baudrate, check CAN clock and baudrate"); + return HAL_ERROR; + } + + __HAL_RCC_FDCAN_CLK_DISABLE(); // Disable for startup, sample calculation were done + + hCAN1.Instance = FDCAN2; // The FDCAN device used + hCAN1.Init.ClockDivider = FDCAN_CLOCK_DIV1; // Clock divider 1 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 + hCAN1.Init.FrameFormat = FDCAN_FRAME_CLASSIC; // FDCAN_FRAME_CLASSIC / FDCAN_FRAME_FD_BRS / FDCAN_FRAME_FD_NO_BRS (Bit Rate Switching) + hCAN1.Init.Mode = FDCAN_MODE_NORMAL; // FDCAN_MODE_NORMAL / FDCAN_MODE_EXTERNAL_LOOPBACK / FDCAN_MODE_INTERNAL_LOOPBACK / FDCAN_MODE_BUS_MONITORING / FDCAN_MODE_RESTRICTED_OPERATION + hCAN1.Init.AutoRetransmission = DISABLE; // Auto Retransmission of message if error occured, can cause lockup + hCAN1.Init.TransmitPause = DISABLE; // Transmit Pause to allow for other device to send message + hCAN1.Init.ProtocolException = DISABLE; // ProtocolException handling + + hCAN1.Init.NominalPrescaler = 1; // Arbitration/data clock prescaler (1-512) + hCAN1.Init.NominalSyncJumpWidth = 1; // Arbitration/data Sync Jump Width (1-128) + hCAN1.Init.NominalTimeSeg1 = seg1; // Arbitration/data period 1 (2-256) + hCAN1.Init.NominalTimeSeg2 = seg2; // Arbitration/data period 2 (2-128) + + /* Not used, no bitrate switching + hCAN1.Init.DataPrescaler = 4; // Arbitration/data clock prescaler (1-32) + hCAN1.Init.DataSyncJumpWidth = 1; // Arbitration/data sync jump width (1-16) + hCAN1.Init.DataTimeSeg1 = 13; // Arbitration/data period 1 (2-32) + hCAN1.Init.DataTimeSeg2 = 2; // Arbitration/data period 2 (2-16) */ + + hCAN1.Init.StdFiltersNbr = 2; // Number of standard ID frame filters, 2 for FIFO toggling + hCAN1.Init.ExtFiltersNbr = 2; // Number of extended ID frame filters, 2 for FIFO toggling + hCAN1.Init.TxFifoQueueMode = FDCAN_TX_FIFO_OPERATION; // Queue mode: FDCAN_TX_FIFO_OPERATION / FDCAN_TX_QUEUE_OPERATION + // "FIFO OPERATION" means send in order of queueing, "QUEUE OPERATION" means send in order of ID priority + + status = HAL_FDCAN_Init(&hCAN1); // calls HAL_FDCAN_MspInit + if (status != HAL_OK) return status; + + FDCAN_FilterTypeDef sFilterConfig; // Configure RX message filter + sFilterConfig.IdType = FDCAN_EXTENDED_ID; // Filter to FIFO1 if higest ID bit is set + sFilterConfig.FilterIndex = 0; // Exteneded filter ID 0 + sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1; // Filter to FIFO1 + sFilterConfig.FilterID1 = 0x10000000; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + sFilterConfig.FilterID2 = 0x10000000; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + +//sFilterConfig.IdType = FDCAN_EXTENDED_ID; // Filter all remaining messages to FIFO0 + sFilterConfig.FilterIndex = 1; // Exteneded filter ID 1 +//sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; // Remaining messages go to FIFO0 + sFilterConfig.FilterID1 = 0; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + sFilterConfig.FilterID2 = 0; // Range: 0 - 0x1FFF FFFF, 0=don'tcare + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + + sFilterConfig.IdType = FDCAN_STANDARD_ID; // Filter to FIFO1 if higest ID bit is set + sFilterConfig.FilterIndex = 0; // Standard filter ID 0 + sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO1; // Filter to FIFO1 + sFilterConfig.FilterID1 = 0b10000000000; // Range: 0 - 0x7FF, 0=don't care + sFilterConfig.FilterID2 = 0b10000000000; // Range: 0 - 0x7FF, 0=don't care + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + +//sFilterConfig.IdType = FDCAN_STANDARD_ID; // Filter all remaining messages to FIFO0 + sFilterConfig.FilterIndex = 1; // Standard filter ID 1 +//sFilterConfig.FilterType = FDCAN_FILTER_MASK; // FDCAN_FILTER_MASK / FDCAN_FILTER_RANGE + sFilterConfig.FilterConfig = FDCAN_FILTER_TO_RXFIFO0; // Remaining messages go to FIFO0 + sFilterConfig.FilterID1 = 0; // Range: 0 - 0x7FF, 0=don't care + sFilterConfig.FilterID2 = 0; // Range: 0 - 0x7FF, 0=don't care + status = HAL_FDCAN_ConfigFilter(&hCAN1, &sFilterConfig); + if (status != HAL_OK) return status; + + status = HAL_FDCAN_Start(&hCAN1); + if (status != HAL_OK) return status; + + // Activate RX FIFO0 new message interrupt + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO0_NEW_MESSAGE, 0); // Calls TIM16_IRQHandler (STM32G0xx) + if (status != HAL_OK) return status; + + // Activate RX FIFO0 message lost interrupt + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO0_MESSAGE_LOST, 0); // Calls TIM16_IRQHandler (STM32G0xx) + if (status != HAL_OK) return status; + + // Activate RX FIFO1 new message interrupt + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO1_NEW_MESSAGE, 0); // Calls TIM16_IRQHandler (STM32G0xx) + if (status != HAL_OK) return status; + + // Activate RX FIFO1 message lost interrupt + status = HAL_FDCAN_ActivateNotification(&hCAN1, FDCAN_IT_RX_FIFO1_MESSAGE_LOST, 0); // Calls TIM16_IRQHandler (STM32G0xx) + if (status != HAL_OK) return status; + + #if ENABLED(CAN_DEBUG) + + SERIAL_ECHOLNPGM(">>> Voltage Scaling: VOS", (PWR->CR1 & PWR_CR1_VOS_Msk) >> PWR_CR1_VOS_Pos); + SERIAL_ECHOLNPGM(">>> FDCAN Peripheral Clock: ", HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN) / 1000000, "MHz"); + SERIAL_ECHOLNPGM(">>> FDCAN Timing. Prescaler: ", hCAN1.Init.NominalPrescaler, " SJW: ", hCAN1.Init.NominalSyncJumpWidth, " SEG1: ", hCAN1.Init.NominalTimeSeg1, " SEG2: ", hCAN1.Init.NominalTimeSeg2); + SERIAL_ECHOLNPGM(">>> FDCAN Bit sample point: ", 100 * (hCAN1.Init.NominalSyncJumpWidth + hCAN1.Init.NominalTimeSeg1)/ (hCAN1.Init.NominalSyncJumpWidth + hCAN1.Init.NominalTimeSeg1 + hCAN1.Init.NominalTimeSeg2), "%"); + uint32_t baudrate = HAL_RCCEx_GetPeriphCLKFreq(RCC_PERIPHCLK_FDCAN) / hCAN1.Init.NominalPrescaler / (hCAN1.Init.NominalSyncJumpWidth + hCAN1.Init.NominalTimeSeg1 + hCAN1.Init.NominalTimeSeg2); + SERIAL_ECHOLNPGM(">>> FDCAN BAUDRATE: ", baudrate); + + if (baudrate != CAN_BAUDRATE) { + SERIAL_ECHOLNPGM(">>> Toolhead ", CAN_ERROR_MSG_INVALID_BAUDRATE, ": ", baudrate, " CAN_BAUDRATE=", CAN_BAUDRATE); + CAN_toolhead_error_code |= CAN_ERROR_TOOLHEAD_INVALID_BAUDRATE; + } + + #endif + + #ifdef CAN_LED_PIN + pinMode(CAN_LED_PIN, OUTPUT); + digitalWrite(CAN_LED_PIN, HIGH); + #endif + + return status; +} // CAN_toolhead_start + +// Send an IO status update to the host, and the E0 temperature if requested +void CAN_toolhead_send_update(bool tempUpdate) { // Called from temperature ISR! + // Send a IO/temp report from the toolhead to the host + FDCAN_TxHeaderTypeDef CanTxHeader; + uint8_t can_tx_buffer[8]; // Transmit FDCAN message buffer + + // Initialize standard TxHeader values + CanTxHeader.FDFormat = FDCAN_CLASSIC_CAN; // FDCAN_CLASSIC_CAN / FDCAN_FD_CAN + CanTxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; // FDCAN_NO_TX_EVENTS / FDCAN_STORE_TX_EVENTS + CanTxHeader.MessageMarker = 0; // 0-0xFF for tracking messages in FIFO buffer + CanTxHeader.ErrorStateIndicator = FDCAN_ESI_PASSIVE; // FDCAN_ESI_PASSIVE / FDCAN_ESI_ACTIVE + CanTxHeader.TxFrameType = FDCAN_DATA_FRAME; // FDCAN_DATA_FRAME / FDCAN_REMOTE_FRAME + CanTxHeader.IdType = FDCAN_STANDARD_ID; // FDCAN_STANDARD_ID / FDCAN_EXTENDED_ID + CanTxHeader.BitRateSwitch = FDCAN_BRS_OFF; // FDCAN_BRS_OFF / FDCAN_BRS_ON + + if (tempUpdate) { // Add temperature update to FDCAN message, 4 bytes + float * fp = (float *)can_tx_buffer; // Point to FDCAN TX buffer + *fp = thermalManager.degHotend(0); // Copy temp to can_tx_buffer + CanTxHeader.DataLength = FDCAN_DLC_BYTES_4; // Hotend temp in payload only + } + else + CanTxHeader.DataLength = FDCAN_DLC_BYTES_0; // 0 data byte FDCAN message, virtual IO is encoded in the message ID + + // Message might be important, don't wait, make room for the message + if (!HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1)) { // No TX slots available + HAL_FDCAN_AbortTxRequest(&hCAN1, FDCAN_TX_BUFFER0 | FDCAN_TX_BUFFER1 | FDCAN_TX_BUFFER2); // Flush all message buffers + CAN_toolhead_error_code |= CAN_ERROR_TOOLHEAD_TX_FIFO_OVERFLOW; + } + + CanTxHeader.Identifier = CAN_get_virtual_IO(tempUpdate); + + if (CAN_request_time_sync) { + + // For time sync request, wait for empty TX slot before requesting + if (HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1) == CAN_FIFO_DEPTH) { + CanTxHeader.Identifier |= (1 << CAN_ID_REQUEST_TIME_SYNC_BIT_POS); // Issue time sync request + + CAN_request_time_sync = false; + NTP[0] = micros(); // Store time sync request message send time + } + } + + HAL_FDCAN_AddMessageToTxFifoQ(&hCAN1, &CanTxHeader, can_tx_buffer); + +} // CAN_toolhead_send_update + +// Send a string message to the host +void CAN_toolhead_send_string(const char * message) { + + FDCAN_TxHeaderTypeDef CanTxHeader; + uint8_t can_tx_buffer[8]; // Transmit CAN message buffer + + CAN_string_buffer.append(message); + + // Make sure there is a '\n' at the end of the string + if (CAN_string_buffer[CAN_string_buffer.length() - 1] != '\n') { + if (CAN_string_buffer.length() == CAN_MAX_STRING_MESSAGE_LENGTH) + CAN_string_buffer.trunc(CAN_MAX_STRING_MESSAGE_LENGTH - 1); + CAN_string_buffer.append('\n'); + } + + SERIAL_ECHOPGM(">>> STRING MSG TO HOST: "); + CAN_string_buffer.echo(); +} + +void CAN_send_next_string_part() { + + uint32_t len = CAN_string_buffer.length(); + + FDCAN_TxHeaderTypeDef CanTxHeader; + uint8_t can_tx_buffer[8]; // Transmit CAN message buffer + + // Initialize standard TX header values + CanTxHeader.FDFormat = FDCAN_CLASSIC_CAN; // FDCAN_CLASSIC_CAN / FDCAN_FD_CAN + CanTxHeader.TxEventFifoControl = FDCAN_NO_TX_EVENTS; // FDCAN_NO_TX_EVENTS / FDCAN_STORE_TX_EVENTS + CanTxHeader.MessageMarker = 0; // 0-0xFF for tracking messages in FIFO buffer + CanTxHeader.ErrorStateIndicator = FDCAN_ESI_PASSIVE; // FDCAN_ESI_PASSIVE / FDCAN_ESI_ACTIVE + CanTxHeader.TxFrameType = FDCAN_DATA_FRAME; // FDCAN_DATA_FRAME / FDCAN_REMOTE_FRAME + CanTxHeader.IdType = FDCAN_STANDARD_ID; // FDCAN_STANDARD_ID / FDCAN_EXTENDED_ID + CanTxHeader.BitRateSwitch = FDCAN_BRS_OFF; // FDCAN_BRS_OFF / FDCAN_BRS_ON + + uint32_t c = MIN(8, len); + CanTxHeader.DataLength = (c << CAN_DATALENGTH_OFFSET); // Max message length is 8 bytes (CAN), offset is 16 bits into the DataLength variable + + CanTxHeader.Identifier = CAN_get_virtual_IO(false) | CAN_ID_STRING_MESSAGE_BIT_MASK; + + // Low priority message, wait until TX FIFO is completely empty before sending the message + if (HAL_FDCAN_GetTxFifoFreeLevel(&hCAN1) == CAN_FIFO_DEPTH) { + HAL_FDCAN_AddMessageToTxFifoQ(&hCAN1, &CanTxHeader, (uint8_t *)&CAN_string_buffer); // Send message + + if (c < 8) + CAN_string_buffer.clear(); + else + strncpy(CAN_string_buffer, (char *)(&CAN_string_buffer + c), len - c + 1); + } + +} // CAN_send_next_string_part + +void CAN_process_time_sync() { + // NTP style time sync + // NTP[0] = local time sync request time + // NTP[1] = host time sync receive time + // NTP[2] = host time sync reply time + // NTP[3] = local time stamp receive time + NTP[0] += CAN_NTP_time_offset; // Adjust local time stamps with CAN_NTP_time_offset + NTP[3] += CAN_NTP_time_offset; // Adjust local time stamps with CAN_NTP_time_offset + int CAN_local_time_adjustment = (NTP[1] - NTP[0] + NTP[2] - NTP[3]) >> 1; + CAN_NTP_time_offset += CAN_local_time_adjustment; + uint32_t CAN_Round_trip_delay = (NTP[3] - NTP[0] - NTP[2] + NTP[1]); + SERIAL_ECHOLNPGM(">>> t0: ", NTP[0], " us (Local time sync request time)"); + SERIAL_ECHOLNPGM(">>> t1: ", NTP[1], " us (remote time sync arrival time)"); + SERIAL_ECHOLNPGM(">>> t2: ", NTP[2], " us (remote time sync response time)"); + SERIAL_ECHOLNPGM(">>> t3: ", NTP[3], " us (local time sync reply receive time)"); + + if (CAN_NTP_clock_drift) + SERIAL_ECHOLNPGM(">>> Predicted time adjustment: ", CAN_NTP_clock_drift * float(NTP[0] - NTP[4]) / 1000000.0 , " us"); + + SERIAL_ECHOPGM(">>> Local time adjustment: ", CAN_local_time_adjustment, " us"); + if (NTP[4]) { + SERIAL_ECHOLNPGM(" after ", ftostr42_52(float(NTP[0] - NTP[4]) / 1000000.0), " seconds"); + CAN_NTP_clock_drift = CAN_local_time_adjustment / (float(NTP[0] - NTP[4]) / 1000000.0); // Calculate drift again + SERIAL_ECHOLNPGM(">>> Clock drift: ", CAN_NTP_clock_drift, " us/s"); + } + else + SERIAL_EOL(); + + SERIAL_ECHOLNPGM(">>> Time offset: ", CAN_NTP_time_offset, " us"); + SERIAL_ECHOLNPGM(">>> Round trip delay: ", CAN_Round_trip_delay, " us"); + SERIAL_ECHOLNPGM(">>> Host response time: ", (NTP[2] - NTP[1]), " us"); + + NTP[4] = NTP[0] + CAN_local_time_adjustment; // Store previous time sync request time + NTP[3] = 0; + +} + +void CAN_handle_errors() { + + uint32_t ms = millis(); + + if (hCAN1.ErrorCode) { + + if (ELAPSED(ms, CAN_next_error_message_time)) { // 12 seconds repeat + MString<40> tmp_string(F("Error: CAN Error Code = "), hCAN1.ErrorCode); + CAN_toolhead_send_string(tmp_string); + CAN_next_error_message_time += 12000; // Delay repeat of message + } + } + + if (CAN_toolhead_error_code) { + + if (ELAPSED(ms, CAN_next_led_flash_time)) { // Flash LED fast on error + TOGGLE(LED_PIN); + CAN_next_led_flash_time = ms + 100; + } + + if (CAN_toolhead_error_code != CAN_previous_error_code) { // Show message on new error code + + #if ENABLED(CAN_DEBUG) + + if (CAN_toolhead_error_code & CAN_ERROR_TOOLHEAD_RX_FIFO_OVERFLOW) { + CAN_toolhead_send_string(CAN_ERROR_MSG_RX_FIFO_OVERFLOW); + SERIAL_ECHOLNPGM(">>> ", CAN_ERROR_MSG_RX_FIFO_OVERFLOW); + } + + if (CAN_toolhead_error_code & CAN_ERROR_TOOLHEAD_TX_FIFO_OVERFLOW) { + CAN_toolhead_send_string(CAN_ERROR_MSG_TX_FIFO_OVERFLOW); + SERIAL_ECHOLNPGM(">>> ", CAN_ERROR_MSG_TX_FIFO_OVERFLOW); + } + + if (CAN_toolhead_error_code & CAN_ERROR_TOOLHEAD_INCOMPLETE_GCODE_RECEIVED) { + CAN_toolhead_send_string(CAN_ERROR_MSG_INCOMPLETE_GCODE); + SERIAL_ECHOLNPGM(">>> ", CAN_ERROR_MSG_INCOMPLETE_GCODE); + } + + if (CAN_toolhead_error_code & CAN_ERROR_TOOLHEAD_MARLIN_CMD_BUFFER_OVERFLOW) { + CAN_toolhead_send_string(CAN_ERROR_MSG_INCOMPLETE_GCODE); + SERIAL_ECHOLNPGM(">>> ", CAN_ERROR_MSG_INCOMPLETE_GCODE); + } + #else + SString<40> string_buffer(F("Error: TOOLHEAD error code="), CAN_toolhead_error_code); + CAN_toolhead_send_string(CAN_string_buffer); + CAN_string_buffer.echoln(); + #endif + CAN_previous_error_code = CAN_toolhead_error_code; + } + } +} // CAN_handle_errors + +void CAN_toolhead_idle() { // Called from MarlinCore.cpp + + // Send temperature update to host + if (ELAPSED(millis(), CAN_next_temp_report_time)) { + CAN_toolhead_send_update(true); + CAN_next_temp_report_time = millis() + CAN_TEMPERATURE_REPORT_INTERVAL; + } + + // Process any new CAN messages + if (CAN_queue_head != CAN_queue_tail) + process_can_queue(); + + // Send next part of a string message to the host + if (CAN_string_buffer.length()) { + if (ELAPSED(millis(), CAN_send_next_string_part_time)) { + CAN_send_next_string_part(); + CAN_send_next_string_part_time = millis() + 3; // Delay a bit, don't overload the half-duplex CAN bus + } + } + + // NTP style time sync + if (NTP[3]) // Indicates a host timestamp was received + CAN_process_time_sync(); + + CAN_handle_errors(); + +} // CAN_send_string + +#endif // CAN_TOOLHEAD diff --git a/Marlin/src/HAL/STM32/Servo.cpp b/Marlin/src/HAL/STM32/Servo.cpp index 4f026ffc6df4..b7bace17579a 100644 --- a/Marlin/src/HAL/STM32/Servo.cpp +++ b/Marlin/src/HAL/STM32/Servo.cpp @@ -29,6 +29,10 @@ #include "Servo.h" +#if ENABLED(CAN_HOST) + #include "../shared/CAN.h" +#endif + static uint_fast8_t servoCount = 0; static libServo *servos[NUM_SERVOS] = {0}; constexpr millis_t servoDelay[] = SERVO_DELAY; @@ -71,6 +75,18 @@ int8_t libServo::attach(const int pin, const int min, const int max) { } void libServo::move(const int value) { + + #if ENABLED(CAN_HOST) // Forward direct Servo command to head + constexpr int angles[2] = Z_SERVO_ANGLES; + // Translate M280 S10 to M401, M280 S90 to M402 + if (value == angles[0]) + CAN_host_send_gcode_2params('M', 401, 0, 0, 0, 0); // Deploy Angle: Send "M401" instead, enables interrupt etc. + else if (value == angles[1]) + CAN_host_send_gcode_2params('M', 402, 0, 0, 0, 0); // Stow Angle: Send "M402" instead, enables interrupt etc. + else + CAN_host_send_gcode_2params('M', 280, 'S', value, 'P', 0); // M280 S[value] P0 + #endif + if (attach(0) >= 0) { stm32_servo.write(value); safe_delay(delay); diff --git a/Marlin/src/HAL/STM32/inc/SanityCheck.h b/Marlin/src/HAL/STM32/inc/SanityCheck.h index e35b4e59cf81..bdcc6ab9f080 100644 --- a/Marlin/src/HAL/STM32/inc/SanityCheck.h +++ b/Marlin/src/HAL/STM32/inc/SanityCheck.h @@ -36,8 +36,8 @@ #error "SDCARD_EEPROM_EMULATION requires SDSUPPORT. Enable SDSUPPORT or choose another EEPROM emulation." #endif -#if !defined(STM32F4xx) && ENABLED(FLASH_EEPROM_LEVELING) - #error "FLASH_EEPROM_LEVELING is currently only supported on STM32F4 hardware." +#if NONE(STM32F4xx, STM32H7xx) && ENABLED(FLASH_EEPROM_LEVELING) + #error "FLASH_EEPROM_LEVELING is currently only supported on STM32F4/H7 hardware." // IRON #endif #if ENABLED(SERIAL_STATS_MAX_RX_QUEUED) diff --git a/Marlin/src/HAL/shared/CAN.h b/Marlin/src/HAL/shared/CAN.h new file mode 100644 index 000000000000..051100972fe2 --- /dev/null +++ b/Marlin/src/HAL/shared/CAN.h @@ -0,0 +1,106 @@ +/** + * Marlin 3D Printer Firmware + * Copyright (c) 2024 MarlinFirmware [https://github.com/MarlinFirmware/Marlin] + * + * Based on Sprinter and grbl. + * Copyright (c) 2011 Camiel Gubbels / Erik van der Zalm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#pragma once + +#include "../../inc/MarlinConfigPre.h" + +#define CAN_HOST_MAX_STRING_MSG_LENGTH 128 // Max string message length to receive from the toolhead +#define CAN_HOST_GCODE_TIME_SYNC_NO 7777 // A unique unused Gcode number to trigger a time sync +#define CAN_HOST_CONFIGURATION_COMPLETE 7778 // Signal the configuration is complete +#define CAN_HOST_MAX_WAIT_TIME 25 // Time in ms to wait for CAN FIFO buffers +#define CAN_HOST_E0_TEMP_UPDATE_WATCHDOG_TIME 3000 // An E0 temp update must be received withing this time +#define CAN_HOST_ERROR_REPEAT_TIME 10000 // Time between report repeats of an error message + +#define STDID_FIFO_TOGGLE_BIT 0b10000000000 +#define EXTID_FIFO_TOGGLE_BIT 0x10000000 + +#define CAN_ID_IO_MASK 0b11111 // Mask for the 5 virtual IO bits +#define CAN_ID_GCODE_MASK 0b111111111111111 // Gcode type and number mask +#define CAN_ID_GCODE_NUMBER_MASK 0b1111111111111 // Gcode number mask +#define CAN_ID_PARAMETER_LETTER_MASK 0b11111 +#define CAN_ID_PARAMETER_COUNT_MASK 0b111 // Parameter count +#define CAN_ID_GCODE_TYPE_MASK 0b11 // Gcode type mask + +#define CAN_ID_PROBE_BIT_MASK (1 << 0) // Virtual IO bit for Z-probe pin +#define CAN_ID_PROBE_BIT_POS 0 +#define CAN_ID_FILAMENT_BIT_MASK (1 << 1) // Virtual IO bit for filament pin +#define CAN_ID_FILAMENT_BIT_POS 1 +#define CAN_ID_X_ENDSTOP_BIT_MASK (1 << 2) // Virtual IO bit for X-min pin +#define CAN_ID_X_ENDSTOP_BIT_POS 2 +#define CAN_ID_Y_ENDSTOP_BIT_MASK (1 << 3) // Virtual IO bit for Y-min pin +#define CAN_ID_Y_ENDSTOP_BIT_POS 3 +#define CAN_ID_Z_ENDSTOP_BIT_MASK (1 << 4) // Virtual IO bit for Z-min pin +#define CAN_ID_Z_ENDSTOP_BIT_POS 4 +#define CAN_ID_STRING_MESSAGE_BIT_MASK (1 << 5) // Signals the toolhead sent a string message +#define CAN_ID_STRING_MESSAGE_BIT_POS 5 +#define CAN_ID_REQUEST_SETUP_BIT_MASK (1 << 6) // Signals the toolhead requests setup information +#define CAN_ID_REQUEST_SETUP_BIT_POS 6 +#define CAN_ID_TMC_OT_BIT_MASK (1 << 7) // Signals the toolhead signals a TMC Over-Temp error +#define CAN_ID_TMC_OT_BIT_POS 7 +#define CAN_ID_REQUEST_TIME_SYNC_BIT_MASK (1 << 8) // Signals the toolhead requested a time sync +#define CAN_ID_REQUEST_TIME_SYNC_BIT_POS 8 +#define CAN_ID_ERROR_BIT_MASK (1 << 9) // Signals the toolhead encountered an error +#define CAN_ID_ERROR_BIT_POS 9 + +#define CAN_ID_PARAMETER1_BIT_POS 0 +#define CAN_ID_PARAMETER2_BIT_POS 5 +#define CAN_ID_GCODE_NUMBER_BIT_POS 10 +#define CAN_ID_GCODE_TYPE_BIT_POS 23 +#define CAN_ID_PARAMETER_COUNT_BIT_POS 25 + +#define CAN_ID_GCODE_TYPE_D 0 +#define CAN_ID_GCODE_TYPE_G 1 +#define CAN_ID_GCODE_TYPE_M 2 +#define CAN_ID_GCODE_TYPE_T 3 + +// Host error messages +#define CAN_ERROR_HOST_RX_FIFO_OVERFLOW (1 << 0) // Incoming message lost +#define CAN_ERROR_HOST_TX_MSG_DROPPED (1 << 1) // Outgoing message dropped +#define CAN_ERROR_HOST_INVALID_GCODE (1 << 2) // Gcode could not be sent over CANBUS +#define CAN_ERROR_HOST_INVALID_BAUDRATE (1 << 3) // Generated baudrate doesn't match CAN_BAUDRATE + +// Toolhead error messages +#define CAN_ERROR_TOOLHEAD_RX_FIFO_OVERFLOW (1 << 4) +#define CAN_ERROR_TOOLHEAD_TX_FIFO_OVERFLOW (1 << 5) +#define CAN_ERROR_TOOLHEAD_INCOMPLETE_GCODE_RECEIVED (1 << 6) +#define CAN_ERROR_TOOLHEAD_MARLIN_CMD_BUFFER_OVERFLOW (1 << 7) +#define CAN_ERROR_TOOLHEAD_INVALID_BAUDRATE (1 << 8) // Generated baudrate doesn't match CAN_BAUDRATE + +// CAN error messsages +#define CAN_ERROR_MSG_RX_FIFO_OVERFLOW "CAN RX FIFO overflow" +#define CAN_ERROR_MSG_TX_FIFO_OVERFLOW "CAN TX FIFO overflow" +#define CAN_ERROR_MSG_INCOMPLETE_GCODE "Incomplete Gcode message received" +#define CAN_ERROR_MSG_MARLIN_CMM_BUF_OVERFLOW "Marlin CMD buffer overflow" +#define CAN_ERROR_MSG_INVALID_BAUDRATE "Incorrect CAN baudrate" + +void CAN_host_idle(); // CAN idle task +void CAN_host_send_setup(bool change_status); // Send configuration to toolhead +uint32_t CAN_host_get_iostate(); // Read the CAN virtual IO state +HAL_StatusTypeDef CAN_host_start(); // Start the CAN device +HAL_StatusTypeDef CAN_host_stop(); // Stop the CAN device +HAL_StatusTypeDef CAN_host_send_gcode(); // Send Gcode to the toolhead +HAL_StatusTypeDef CAN_host_send_gcode_2params(uint32_t Gcode_type, uint32_t Gcode_no, uint32_t parameter1, float value1, uint32_t parameter2, float value2); + +HAL_StatusTypeDef CAN_toolhead_start(); // Start the CAN device +void CAN_toolhead_send_update(bool tempUpdate); // Send an IO and temp update to the host +void CAN_toolhead_send_string(const char * message); // Send CAN string to host +void CAN_toolhead_idle(); \ No newline at end of file diff --git a/Marlin/src/MarlinCore.cpp b/Marlin/src/MarlinCore.cpp index 140423f25c76..cef754111980 100644 --- a/Marlin/src/MarlinCore.cpp +++ b/Marlin/src/MarlinCore.cpp @@ -34,6 +34,14 @@ #include "HAL/shared/esp_wifi.h" #include "HAL/shared/cpu_exception/exception_hook.h" +#if ANY(CAN_HOST, CAN_TOOLHEAD) + #include "HAL/shared/CAN.h" +#endif + +#if ENABLED(HAS_ADXL345_ACCELEROMETER) + #include "feature/accelerometer/acc_adxl345.h" +#endif + #if ENABLED(WIFISUPPORT) #include "HAL/shared/esp_wifi.h" #endif @@ -882,6 +890,10 @@ void idle(const bool no_stepper_sleep/*=false*/) { // Manage Fixed-time Motion Control TERN_(FT_MOTION, ftMotion.loop()); + TERN_(CAN_HOST, CAN_host_idle()); + + TERN_(CAN_TOOLHEAD, CAN_toolhead_idle()); + IDLE_DONE: TERN_(MARLIN_DEV_MODE, idle_depth--); @@ -1236,6 +1248,23 @@ void setup() { SETUP_RUN(hal.init()); + #if ENABLED(CAN_HOST) + SERIAL_ECHOLN( + F(">>> CAN Start: "), + CAN_host_start() == HAL_OK ? F("OK") : F("FAILED!") + ); + #endif + + #if ENABLED(CAN_TOOLHEAD) + SERIAL_ECHOLN( F(">>> CAN Start: "), + CAN_toolhead_start() == HAL_OK ? F("OK") : F("FAILED!") + ); + #endif + + #if ENABLED(HAS_ADXL345_ACCELEROMETER) + adxl345.begin(); + #endif + // Init and disable SPI thermocouples; this is still needed #if TEMP_SENSOR_IS_MAX_TC(0) || (TEMP_SENSOR_IS_MAX_TC(REDUNDANT) && REDUNDANT_TEMP_MATCH(SOURCE, E0)) OUT_WRITE(TEMP_0_CS_PIN, HIGH); // Disable diff --git a/Marlin/src/feature/runout.h b/Marlin/src/feature/runout.h index 52a9020830b7..1fa351f42135 100644 --- a/Marlin/src/feature/runout.h +++ b/Marlin/src/feature/runout.h @@ -33,6 +33,10 @@ #include "pause.h" // for did_pause_print #include "../MarlinCore.h" // for printingIsActive() +#if ENABLED(CAN_HOST) + #include "../HAL/shared/CAN.h" +#endif + #include "../inc/MarlinConfig.h" #if ENABLED(EXTENSIBLE_UI) @@ -51,7 +55,12 @@ #define HAS_FILAMENT_SWITCH 1 #endif -#define FILAMENT_IS_OUT() (READ(FIL_RUNOUT_PIN) == FIL_RUNOUT_STATE) +#if ENABLED(CAN_HOST) + #define FILAMENT_IS_OUT() (bool(CAN_host_get_iostate() & CAN_ID_FILAMENT_MASK) == FIL_RUNOUT_STATE) + #define RUNOUT_STATE(N) bool(CAN_host_get_iostate() & CAN_ID_FILAMENT_MASK) // CAN Virtual Filament Runout pin +#else + #define FILAMENT_IS_OUT(N...) (READ(FIL_RUNOUT##N##_PIN) == FIL_RUNOUT##N##_STATE) +#endif typedef Flags< #if NUM_MOTION_SENSORS > NUM_RUNOUT_SENSORS @@ -207,7 +216,7 @@ class FilamentSensorBase { // Return a bitmask of runout pin states static uint8_t poll_runout_pins() { - #define _OR_RUNOUT(N) | (READ(FIL_RUNOUT##N##_PIN) ? _BV((N) - 1) : 0) + #define _OR_RUNOUT(N) | (RUNOUT_STATE(N) ? _BV((N) - 1) : 0) return (0 REPEAT_1(NUM_RUNOUT_SENSORS, _OR_RUNOUT)); #undef _OR_RUNOUT } diff --git a/Marlin/src/gcode/gcode.cpp b/Marlin/src/gcode/gcode.cpp index 9fed4dcada3f..7448997358a5 100644 --- a/Marlin/src/gcode/gcode.cpp +++ b/Marlin/src/gcode/gcode.cpp @@ -69,6 +69,11 @@ GcodeSuite gcode; #include "../feature/fancheck.h" #endif +#if ENABLED(CAN_HOST) + #include "../HAL/shared/CAN.h" + #include "../libs/buzzer.h" +#endif + #include "../MarlinCore.h" // for idle, kill // Inactivity shutdown @@ -323,6 +328,15 @@ void GcodeSuite::process_parsed_command(const bool no_ok/*=false*/) { KEEPALIVE_STATE(IN_HANDLER); + #if ENABLED(CAN_HOST) + if (CAN_host_send_gcode() != HAL_OK) { // Send command to toolhead + SERIAL_ECHOLN(F("Error: CAN failed to send \""), parser.command_ptr, '"'); + #if ENABLED(CAN_DEBUG) + BUZZ(1, SOUND_ERROR); + #endif + } + #endif + /** * Block all Gcodes except M511 Unlock Printer, if printer is locked * Will still block Gcodes if M511 is disabled, in which case the printer should be unlocked via LCD Menu diff --git a/Marlin/src/gcode/host/M115.cpp b/Marlin/src/gcode/host/M115.cpp index a4a2ac405f57..f5bf4b09466c 100644 --- a/Marlin/src/gcode/host/M115.cpp +++ b/Marlin/src/gcode/host/M115.cpp @@ -35,6 +35,10 @@ #include "../../feature/caselight.h" #endif +#if ANY(CAN_HOST, CAN_TOOLHEAD) + #include "../../HAL/shared/CAN.h" +#endif + #if !defined(MACHINE_UUID) && ENABLED(HAS_STM32_UID) #include "../../libs/hex_print.h" #endif @@ -76,6 +80,10 @@ void GcodeSuite::M115() { TERN_(IS_CARTESIAN, "Cartesian") \ TERN_(BELTPRINTER, " BELTPRINTER") +#if ENABLED(CAN_TOOLHEAD) + MString<60> buffer("FIRMWARE ", __DATE__, " ", __TIME__, " Thermistor=", TEMP_SENSOR_0); + CAN_toolhead_send_string(buffer); +#endif SERIAL_ECHOPGM("FIRMWARE_NAME:Marlin" " " DETAILED_BUILD_VERSION " (" __DATE__ " " __TIME__ ")" " SOURCE_CODE_URL:" SOURCE_CODE_URL diff --git a/Marlin/src/gcode/temp/M306.cpp b/Marlin/src/gcode/temp/M306.cpp index 12e175420dc2..01707735f4a9 100644 --- a/Marlin/src/gcode/temp/M306.cpp +++ b/Marlin/src/gcode/temp/M306.cpp @@ -27,6 +27,11 @@ #include "../gcode.h" #include "../../lcd/marlinui.h" #include "../../module/temperature.h" +#include "../../libs/numtostr.h" + +#if ENABLED(CAN_TOOLHEAD) + #include "../../HAL/shared/CAN.h" +#endif /** * M306: MPC settings and autotune @@ -57,16 +62,34 @@ void GcodeSuite::M306() { #if ENABLED(MPC_AUTOTUNE) if (parser.seen_test('T')) { - Temperature::MPCTuningType tuning_type; - const uint8_t type = parser.byteval('S', 0); - switch (type) { - case 1: tuning_type = Temperature::MPCTuningType::FORCE_DIFFERENTIAL; break; - case 2: tuning_type = Temperature::MPCTuningType::FORCE_ASYMPTOTIC; break; - default: tuning_type = Temperature::MPCTuningType::AUTO; break; - } - LCD_MESSAGE(MSG_MPC_AUTOTUNE); - thermalManager.MPC_autotune(e, tuning_type); - ui.reset_status(); + + #if ENABLED(CAN_HOST) // Execute MPC autotune on toolhead + + SERIAL_ECHOLNPGM( + ">>> Forwarding M306 to toolhead\n" + ">>> Store MPC setup in the host Configuration.h or use M500\n" + ">>> MPC heater power is: ", p_float_t(MPC_HEATER_POWER, 1), " Watts\n" + ">>> Please wait for the auto tune results..." + ); + + #else + + Temperature::MPCTuningType tuning_type; + const uint8_t type = parser.byteval('S', 0); + switch (type) { + case 1: tuning_type = Temperature::MPCTuningType::FORCE_DIFFERENTIAL; break; + case 2: tuning_type = Temperature::MPCTuningType::FORCE_ASYMPTOTIC; break; + default: tuning_type = Temperature::MPCTuningType::AUTO; break; + } + LCD_MESSAGE(MSG_MPC_AUTOTUNE); + thermalManager.MPC_autotune(e, tuning_type); + ui.reset_status(); + + #if ENABLED(CAN_TOOLHEAD) + M306_report(true); // Report MPC autotune results to CAN host + #endif + #endif + return; } #endif @@ -91,6 +114,11 @@ void GcodeSuite::M306_report(const bool forReplay/*=true*/) { TERN_(MARLIN_SMALL_BUILD, return); report_heading(forReplay, F("Model predictive control")); + + #if ENABLED(CAN_HOST) // MPC Autotune info + if (forReplay) SERIAL_ECHOLNPGM(">>> Host M306 MPC settings:"); + #endif + HOTEND_LOOP() { report_echo_start(forReplay); MPC_t &mpc = thermalManager.temp_hotend[e].mpc; @@ -105,6 +133,23 @@ void GcodeSuite::M306_report(const bool forReplay/*=true*/) { #endif SERIAL_ECHOLNPGM(" H", p_float_t(mpc.filament_heat_capacity_permm, 4)); } + + #if ENABLED(CAN_TOOLHEAD) // Report M306 Autotune results to host + if (forReplay) { + MPC_t &mpc = thermalManager.temp_hotend[0].mpc; + MString<100> buffer(F("M306 E0 P"), p_float_t(mpc.heater_power, 2), + " C", p_float_t(mpc.block_heat_capacity, 2), + " R", p_float_t(mpc.sensor_responsiveness, 4), + " A", p_float_t(mpc.ambient_xfer_coeff_fan0, 4), + #if ENABLED(MPC_INCLUDE_FAN) + " F", p_float_t(mpc.fanCoefficient(), 4), + #endif + " H", p_float_t(mpc.filament_heat_capacity_permm, 4)); + + CAN_toolhead_send_string(buffer); + } +#endif // CAN_TOOLHEAD + } #endif // MPCTEMP diff --git a/Marlin/src/inc/SanityCheck.h b/Marlin/src/inc/SanityCheck.h index 9ec17b28089e..b3576280c806 100644 --- a/Marlin/src/inc/SanityCheck.h +++ b/Marlin/src/inc/SanityCheck.h @@ -245,7 +245,7 @@ static_assert(COUNT(arm) == LOGICAL_AXES, "AXIS_RELATIVE_MODES must contain " _L #if ENABLED(SERIAL_DMA) #ifdef ARDUINO_ARCH_HC32 // checks for HC32 are located in HAL/HC32/inc/SanityCheck.h - #elif DISABLED(HAL_STM32) || NONE(STM32F0xx, STM32F1xx, STM32F2xx, STM32F4xx, STM32F7xx) + #elif DISABLED(HAL_STM32) || NONE(STM32F0xx, STM32F1xx, STM32F2xx, STM32F4xx, STM32F7xx, STM32H7xx) #error "SERIAL_DMA is only available for some STM32 MCUs and requires HAL/STM32." #elif !defined(HAL_UART_MODULE_ENABLED) || defined(HAL_UART_MODULE_ONLY) #error "SERIAL_DMA requires STM32 platform HAL UART (without HAL_UART_MODULE_ONLY)." diff --git a/Marlin/src/module/endstops.cpp b/Marlin/src/module/endstops.cpp index 85e021d661ea..b12c93906951 100644 --- a/Marlin/src/module/endstops.cpp +++ b/Marlin/src/module/endstops.cpp @@ -27,9 +27,10 @@ #include "endstops.h" #include "stepper.h" -#include "../sd/cardreader.h" -#include "temperature.h" -#include "../lcd/marlinui.h" +#if ANY(HAS_STATUS_MESSAGE, VALIDATE_HOMING_ENDSTOPS) + #include "../lcd/marlinui.h" +#endif + #if ENABLED(SOVOL_SV06_RTS) #include "../lcd/sovol_rts/sovol_rts.h" #endif @@ -44,6 +45,8 @@ #if ENABLED(SD_ABORT_ON_ENDSTOP_HIT) #include "printcounter.h" // for print_job_timer + #include "temperature.h" + #include "../sd/cardreader.h" #endif #if ENABLED(BLTOUCH) @@ -54,10 +57,18 @@ #include "../feature/joystick.h" #endif +#if HAS_FILAMENT_SENSOR + #include "../feature/runout.h" +#endif + #if HAS_BED_PROBE #include "probe.h" #endif +#if ENABLED(CAN_TOOLHEAD) + #include "../HAL/shared/CAN.h" +#endif + #define DEBUG_OUT ALL(USE_SENSORLESS, DEBUG_LEVELING_FEATURE) #include "../core/debug_out.h" @@ -77,6 +88,12 @@ Endstops::endstop_mask_t Endstops::live_state = 0; #else #define READ_ENDSTOP(P) READ(P) #endif +#elif ENABLED(CAN_HOST) // Read virtual CAN IO probe status if needed + #if HAS_BED_PROBE + #define READ_ENDSTOP(P) ((P == Z_MIN_PIN) ? PROBE_READ() : READ(P)) + #else + #define READ_ENDSTOP(P) READ(P) + #endif #else #define READ_ENDSTOP(P) READ(P) #endif @@ -375,13 +392,13 @@ void Endstops::event_handler() { #endif SERIAL_EOL(); - TERN_(HAS_STATUS_MESSAGE, + #if HAS_STATUS_MESSAGE ui.status_printf(0, F(S_FMT GANG_N_1(NUM_AXES, " %c") " %c"), GET_TEXT_F(MSG_LCD_ENDSTOPS), NUM_AXIS_LIST_(chrX, chrY, chrZ, chrI, chrJ, chrK, chrU, chrV, chrW) chrP - ) - ); + ); + #endif #if ENABLED(SD_ABORT_ON_ENDSTOP_HIT) if (planner.abort_on_endstop_hit) { @@ -526,7 +543,7 @@ void __O2 Endstops::report_states() { } #undef _CASE_RUNOUT #elif HAS_FILAMENT_SENSOR - print_es_state(READ(FIL_RUNOUT1_PIN) != FIL_RUNOUT1_STATE, F(STR_FILAMENT)); + print_es_state(!FILAMENT_IS_OUT(), F(STR_FILAMENT)); #endif TERN_(BLTOUCH, bltouch._reset_SW_mode()); @@ -664,6 +681,11 @@ void Endstops::update() { // When closing the gap check the enabled probe if (probe_switch_activated()) UPDATE_LIVE_STATE(Z, TERN(USE_Z_MIN_PROBE, MIN_PROBE, MIN)); + + #if ENABLED(CAN_TOOLHEAD) // Forward endstop interrupt to host + CAN_toolhead_send_update(false); // Send Virtual IO update without temperature report + #endif + #endif #if USE_Z_MAX diff --git a/Marlin/src/module/probe.h b/Marlin/src/module/probe.h index 08a02b4d4004..25466feb2d8b 100644 --- a/Marlin/src/module/probe.h +++ b/Marlin/src/module/probe.h @@ -29,6 +29,10 @@ #include "motion.h" +#if ENABLED(CAN_HOST) + #include "../HAL/shared/CAN.h" +#endif + #if ENABLED(BLTOUCH) #include "../feature/bltouch.h" #endif @@ -47,11 +51,14 @@ #if ENABLED(BD_SENSOR) #define PROBE_READ() bdp_state +#elif ENABLED(CAN_HOST) + #define PROBE_READ() bool(CAN_host_get_iostate() & CAN_ID_PROBE_MASK) #elif USE_Z_MIN_PROBE #define PROBE_READ() READ(Z_MIN_PROBE_PIN) #else #define PROBE_READ() READ(Z_MIN_PIN) #endif + #if USE_Z_MIN_PROBE #define PROBE_HIT_STATE Z_MIN_PROBE_ENDSTOP_HIT_STATE #else diff --git a/Marlin/src/module/settings.cpp b/Marlin/src/module/settings.cpp index 51677fdec6b8..27e60be02db0 100644 --- a/Marlin/src/module/settings.cpp +++ b/Marlin/src/module/settings.cpp @@ -735,6 +735,10 @@ void MarlinSettings::postprocess() { TERN_(EXTENSIBLE_UI, ExtUI::onPostprocessSettings()); + #if ENABLED(CAN_HOST) + CAN_host_send_setup(); // Update toolhead settings + #endif + // Refresh mm_per_step with the reciprocal of axis_steps_per_mm // and init stepper.count[], planner.position[] with current_position planner.refresh_positioning(); diff --git a/Marlin/src/module/temperature.cpp b/Marlin/src/module/temperature.cpp index 96295d7db586..af4f5f8dd856 100644 --- a/Marlin/src/module/temperature.cpp +++ b/Marlin/src/module/temperature.cpp @@ -50,6 +50,10 @@ #include "motion.h" #endif +#if ENABLED(CAN_HOST) + #include "../HAL/shared/CAN.h" +#endif + #if ENABLED(DWIN_CREALITY_LCD) #include "../lcd/e3v2/creality/dwin.h" #elif ENABLED(SOVOL_SV06_RTS) @@ -2734,7 +2738,7 @@ void Temperature::updateTemperaturesFromRawValues() { temp_bed.setraw(read_max_tc_bed()); #endif - #if HAS_HOTEND + #if HAS_HOTEND && DISABLED(CAN_HOST) // For CAN Host we'll get temperature from CAN bus HOTEND_LOOP() temp_hotend[e].celsius = analog_to_celsius_hotend(temp_hotend[e].getraw(), e); #endif @@ -2749,7 +2753,8 @@ void Temperature::updateTemperaturesFromRawValues() { TERN_(FILAMENT_WIDTH_SENSOR, filwidth.update_measured_mm()); TERN_(HAS_POWER_MONITOR, power_monitor.capture_values()); - #if HAS_HOTEND + #if HAS_HOTEND && DISABLED(CAN_HOST) // Only for toolhead, no temp sampling on Host + #define _TEMPDIR(N) TEMP_SENSOR_IS_ANY_MAX_TC(N) ? 0 : TEMPDIR(N), static constexpr int8_t temp_dir[HOTENDS] = { REPEAT(HOTENDS, _TEMPDIR) }; @@ -2776,7 +2781,7 @@ void Temperature::updateTemperaturesFromRawValues() { } } - #endif // HAS_HOTEND + #endif // HAS_HOTEND && !CAN_HOST #if ENABLED(THERMAL_PROTECTION_BED) if (TP_CMP(BED, temp_bed.getraw(), temp_sensor_range_bed.raw_max)) @@ -3375,10 +3380,18 @@ void Temperature::disable_all_heaters() { TERN_(PROBING_HEATERS_OFF, pause_heaters(false)); #if HAS_HOTEND + + #if ENABLED(CAN_HOST) // Shut down the hotend in the head too + CAN_host_send_gcode_2params('M', 104, 'S', 0, 0, 0); // M104 S0 .... Switch off hotend heating + CAN_host_send_gcode_2params('M', 107, 0, 0, 0, 0); // M107 ....... Switch off part cooling fan + CAN_host_send_gcode_2params('M', 150, 'R', 255, 0, 0); // M150 R255 .. Set NeoPixel to red + #endif + HOTEND_LOOP() { setTargetHotend(0, e); temp_hotend[e].soft_pwm_amount = 0; } + #endif #if HAS_TEMP_HOTEND @@ -4418,6 +4431,12 @@ void Temperature::isr() { // Poll endstops state, if required endstops.poll(); +#if ENABLED(CAN_TOOLHEAD) + static uint32_t loopCounter = 0; + if ((loopCounter++ % 512) == 0) // Update E0 Temp every 512ms + CAN_Send_Message(true); // Send temp report with IO report +#endif // CAN_TOOLHEAD + // Periodically call the planner timer service routine planner.isr(); } diff --git a/Marlin/src/pins/pins.h b/Marlin/src/pins/pins.h index eb26103380c1..516fe1d71a22 100644 --- a/Marlin/src/pins/pins.h +++ b/Marlin/src/pins/pins.h @@ -562,7 +562,7 @@ // #elif MB(BTT_EBB42_V1_1) - #include "stm32g0/pins_BTT_EBB42_V1_1.h" // STM32G0 env:BTT_EBB42_V1_1_filament_extruder + #include "stm32g0/pins_BTT_EBB42_V1_1.h" // STM32G0 env:BTT_EBB42_V1_1_FDCAN env:BTT_EBB42_V1_1_filament_extruder #elif MB(BTT_SKR_MINI_E3_V3_0) #include "stm32g0/pins_BTT_SKR_MINI_E3_V3_0.h" // STM32G0 env:STM32G0B1RE_btt env:STM32G0B1RE_btt_xfer #elif MB(BTT_MANTA_E3_EZ_V1_0) @@ -798,9 +798,9 @@ #elif MB(MKS_ROBIN_NANO_V3_1) #include "stm32f4/pins_MKS_ROBIN_NANO_V3.h" // STM32F4 env:mks_robin_nano_v3_1 env:mks_robin_nano_v3_1_usb_flash_drive env:mks_robin_nano_v3_1_usb_flash_drive_msc #elif MB(MKS_MONSTER8_V1) - #include "stm32f4/pins_MKS_MONSTER8_V1.h" // STM32F4 env:mks_monster8 env:mks_monster8_usb_flash_drive env:mks_monster8_usb_flash_drive_msc + #include "stm32f4/pins_MKS_MONSTER8_V1.h" // STM32F4 env:mks_monster8 env:mks_monster8_usb_flash_drive env:mks_monster8_usb_flash_drive_msc env:mks_monster8_usb_flash_drive_msc_CAN #elif MB(MKS_MONSTER8_V2) - #include "stm32f4/pins_MKS_MONSTER8_V2.h" // STM32F4 env:mks_monster8 env:mks_monster8_usb_flash_drive env:mks_monster8_usb_flash_drive_msc + #include "stm32f4/pins_MKS_MONSTER8_V2.h" // STM32F4 env:mks_monster8 env:mks_monster8_usb_flash_drive env:mks_monster8_usb_flash_drive_msc env:mks_monster8_usb_flash_drive_msc_CAN #elif MB(ANET_ET4) #include "stm32f4/pins_ANET_ET4.h" // STM32F4 env:Anet_ET4_no_bootloader env:Anet_ET4_OpenBLT #elif MB(ANET_ET4P) diff --git a/Marlin/src/pins/stm32f4/pins_MKS_MONSTER8_common.h b/Marlin/src/pins/stm32f4/pins_MKS_MONSTER8_common.h index b15c7d7288c6..3e403a340cce 100644 --- a/Marlin/src/pins/stm32f4/pins_MKS_MONSTER8_common.h +++ b/Marlin/src/pins/stm32f4/pins_MKS_MONSTER8_common.h @@ -42,7 +42,8 @@ //#define SRAM_EEPROM_EMULATION // Use BackSRAM-based EEPROM emulation //#define FLASH_EEPROM_EMULATION // Use Flash-based EEPROM emulation #define I2C_EEPROM // Need use jumpers set i2c for EEPROM -#define MARLIN_EEPROM_SIZE 0x1000 // 4K +#define MARLIN_EEPROM_SIZE 0x1000U // 4K + #define I2C_SCL_PIN PB8 // I2C_SCL and CAN_RX #define I2C_SDA_PIN PB9 // I2C_SDA and CAN_TX diff --git a/Marlin/src/pins/stm32g0/pins_BTT_EBB42_V1_1.h b/Marlin/src/pins/stm32g0/pins_BTT_EBB42_V1_1.h index 2d6cdb73f43c..53f88673d58a 100644 --- a/Marlin/src/pins/stm32g0/pins_BTT_EBB42_V1_1.h +++ b/Marlin/src/pins/stm32g0/pins_BTT_EBB42_V1_1.h @@ -27,7 +27,7 @@ * This board definition is to facilitate support for a Filament Extrusion * devices, used to convert waste plastic into 3D printable filament. * This board is NOT a general 3D printing controller; it is NOT supported - * as a toolboard via CANBUS (as it was originally designed) or any device + * as a toolboard via CAN bus (as it was originally designed) or any device * that requires kinematics. */ @@ -147,3 +147,6 @@ #define BTN_EN2 PB5 #define BTN_ENC PB6 #endif + +#define CAN_RX PB0 +#define CAN_TX PB1 diff --git a/ini/stm32f4.ini b/ini/stm32f4.ini index 7fdbc6980477..fbe68125092c 100644 --- a/ini/stm32f4.ini +++ b/ini/stm32f4.ini @@ -619,6 +619,12 @@ build_flags = ${stm_flash_drive.build_flags} ${stm32f4_I2C1_CAN.build_flag extends = env:mks_monster8_usb_flash_drive build_flags = ${env:mks_monster8_usb_flash_drive.build_flags} -DUSBD_USE_CDC_MSC + +[env:mks_monster8_usb_flash_drive_msc_CAN] +extends = env:mks_monster8_usb_flash_drive +build_flags = ${env:mks_monster8_usb_flash_drive.build_flags} + -DUSBD_USE_CDC_MSC + -DHAL_CAN_MODULE_ENABLED build_unflags = -DUSBD_USE_CDC # diff --git a/ini/stm32g0.ini b/ini/stm32g0.ini index 4a9d1e4f7ae5..bf20b68f9b7a 100644 --- a/ini/stm32g0.ini +++ b/ini/stm32g0.ini @@ -26,10 +26,9 @@ build_flags = -DPIN_WIRE_SCL=PB3 -DPIN_WIRE_SDA=PB4 # -# BigTreeTech EBB42 V1.1 (STM32G0B1CBT6 ARM Cortex-M0+) -# This board is being used to control Filament extruders. This is not supported for 3D printing, as it has no kinematics control +# BTT EBB32 V1.1 base # -[env:BTT_EBB42_V1_1_filament_extruder] +[BTT_EBB42_V1_1_common] extends = stm32_variant platform = ststm32@17.1.0 platform_packages = framework-arduinoststm32@~4.20600.231001 @@ -44,6 +43,19 @@ debug_tool = stlink upload_protocol = dfu upload_command = dfu-util -a 0 -s 0x08000000:leave -D "$SOURCE" +# +# BigTreeTech EBB42 V1.1 (STM32G0B1CBT6 ARM Cortex-M0+) as a CAN toolboard +# +[env:BTT_EBB42_V1_1_FDCAN] +extends = BTT_EBB42_V1_1_common +build_flags = ${BTT_EBB42_V1_1_common.build_flags} + -DHAL_FDCAN_MODULE_ENABLED +# +# BigTreeTech EBB42 V1.1 (STM32G0B1CBT6 ARM Cortex-M0+) as a filament extruder (no kinematics control) +# +[env:BTT_EBB42_V1_1_filament_extruder] +extends = BTT_EBB42_V1_1_common + # # BigTreeTech SKR Mini E3 V3.0 (STM32G0B0RET6 / STM32G0B1RET6 ARM Cortex-M0+) #