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+)
#