diff --git a/src/atsam/Kconfig b/src/atsam/Kconfig
index 7fac95f50..4a10a0f7d 100644
--- a/src/atsam/Kconfig
+++ b/src/atsam/Kconfig
@@ -50,6 +50,9 @@ config MACH_SAM4E
     select MACH_SAM4
 config MACH_SAME70
     bool
+config HAVE_SAM_CANBUS
+    bool
+    default y if MACH_SAME70
 
 config MCU
     string
@@ -101,6 +104,32 @@ choice
     config ATSAM_SERIAL
         bool "Serial"
         select SERIAL
+    config ATSAM_MMENU_CANBUS_PC12_PD12
+        bool "CAN bus (on PC12/PD12)"
+        depends on HAVE_SAM_CANBUS
+        select CANSERIAL
+    config ATSAM_MMENU_CANBUS_PB3_PB2
+        bool "CAN bus (on PB3/PB2)"
+        depends on HAVE_SAM_CANBUS
+        select CANSERIAL
+    config ATSAM_USBCANBUS
+        bool "USB to CAN bus bridge"
+        depends on HAVE_SAM_CANBUS
+        select USBCANBUS
+endchoice
+choice
+    prompt "CAN bus interface" if USBCANBUS
+    config ATSAM_CMENU_CANBUS_PC12_PD12
+        bool "CAN bus (on PC12/PD12)"
+    config ATSAM_CMENU_CANBUS_PB3_PB2
+        bool "CAN bus (on PB3/PB2)"
 endchoice
 
+config ATSAM_CANBUS_PC12_PD12
+    bool
+    default y if ATSAM_MMENU_CANBUS_PC12_PD12 || ATSAM_CMENU_CANBUS_PC12_PD12
+config ATSAM_CANBUS_PB3_PB2
+    bool
+    default y if ATSAM_MMENU_CANBUS_PB3_PB2 || ATSAM_CMENU_CANBUS_PB3_PB2
+
 endif
diff --git a/src/atsam/Makefile b/src/atsam/Makefile
index 1c32cb40c..7ab69b823 100644
--- a/src/atsam/Makefile
+++ b/src/atsam/Makefile
@@ -3,7 +3,7 @@
 # Setup the toolchain
 CROSS_PREFIX=arm-none-eabi-
 
-dirs-y += src/atsam src/generic
+dirs-y += src/atsam src/generic lib/fast-hash
 dirs-$(CONFIG_MACH_SAM3X) += lib/sam3x/gcc
 dirs-$(CONFIG_MACH_SAM4S) += lib/sam4s/gcc
 dirs-$(CONFIG_MACH_SAM4E) += lib/sam4e/gcc
@@ -18,7 +18,7 @@ CFLAGS-$(CONFIG_MACH_SAM3X) += -Ilib/sam3x/include
 CFLAGS-$(CONFIG_MACH_SAM4S) += -Ilib/sam4s/include
 CFLAGS-$(CONFIG_MACH_SAM4E) += -Ilib/sam4e/include
 CFLAGS-$(CONFIG_MACH_SAME70) += -Ilib/same70b/include
-CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core
+CFLAGS += $(CFLAGS-y) -D__$(MCU)__ -mthumb -Ilib/cmsis-core -Ilib/fast-hash
 
 CFLAGS_klipper.elf += --specs=nano.specs --specs=nosys.specs
 CFLAGS_klipper.elf += -T $(OUT)src/generic/armcm_link.ld
@@ -33,6 +33,10 @@ usb-src-$(CONFIG_MACH_SAM4) := atsam/sam4_usb.c
 usb-src-$(CONFIG_MACH_SAME70) := atsam/sam3_usb.c
 src-$(CONFIG_USBSERIAL) += $(usb-src-y) atsam/chipid.c generic/usb_cdc.c
 src-$(CONFIG_SERIAL) += atsam/serial.c generic/serial_irq.c
+canbus-src-y := generic/canserial.c ../lib/fast-hash/fasthash.c
+canbus-src-y += atsam/fdcan.c atsam/chipid.c
+src-$(CONFIG_USBCANBUS) += $(canbus-src-y) $(usb-src-y) generic/usb_canbus.c
+src-$(CONFIG_CANSERIAL) += $(canbus-src-y) generic/canbus.c
 src-$(CONFIG_MACH_SAM3X) += atsam/adc.c atsam/hard_pwm.c
 src-$(CONFIG_MACH_SAM4)  += atsam/hard_pwm.c
 src-$(CONFIG_MACH_SAM4S) += atsam/adc.c
diff --git a/src/atsam/chipid.c b/src/atsam/chipid.c
index b4ab289ca..dda4c5d00 100644
--- a/src/atsam/chipid.c
+++ b/src/atsam/chipid.c
@@ -5,6 +5,7 @@
 // This file may be distributed under the terms of the GNU GPLv3 license.
 
 #include "generic/irq.h" // irq_disable
+#include "generic/canserial.h" // canserial_set_uuid
 #include "generic/usb_cdc.h" // usb_fill_serial
 #include "generic/usbstd.h" // usb_string_descriptor
 #include "internal.h" // EFC0
@@ -61,7 +62,7 @@ read_chip_id(uint32_t *id)
 void
 chipid_init(void)
 {
-    if (!CONFIG_USB_SERIAL_NUMBER_CHIPID)
+    if (!CONFIG_USB_SERIAL_NUMBER_CHIPID && !CONFIG_CANBUS)
         return;
 
     uint32_t id[4];
@@ -69,6 +70,10 @@ chipid_init(void)
     read_chip_id(id);
     irq_enable();
 
-    usb_fill_serial(&cdc_chipid.desc, ARRAY_SIZE(cdc_chipid.data), id);
+    if (CONFIG_USB_SERIAL_NUMBER_CHIPID)
+        usb_fill_serial(&cdc_chipid.desc, ARRAY_SIZE(cdc_chipid.data), id);
+
+    if (CONFIG_CANBUS)
+        canserial_set_uuid((void*)id, CHIP_UID_LEN);
 }
 DECL_INIT(chipid_init);
diff --git a/src/atsam/fdcan.c b/src/atsam/fdcan.c
new file mode 100644
index 000000000..a536a7be1
--- /dev/null
+++ b/src/atsam/fdcan.c
@@ -0,0 +1,307 @@
+// CANbus support on atsame70 chips
+//
+// Copyright (C) 2021-2022  Kevin O'Connor <kevin@koconnor.net>
+// Copyright (C) 2019 Eug Krashtan <eug.krashtan@gmail.com>
+// Copyright (C) 2020 Pontus Borg <glpontus@gmail.com>
+//
+// This file may be distributed under the terms of the GNU GPLv3 license.
+
+#include "command.h" // DECL_CONSTANT_STR
+#include "generic/armcm_boot.h" // armcm_enable_irq
+#include "generic/canbus.h" // canbus_notify_tx
+#include "generic/canserial.h" // CANBUS_ID_ADMIN
+#include "internal.h" // enable_pclock
+#include "sched.h" // DECL_INIT
+
+
+/****************************************************************
+ * Pin configuration
+ ****************************************************************/
+
+#if CONFIG_ATSAM_CANBUS_PB3_PB2
+ DECL_CONSTANT_STR("RESERVE_PINS_CAN", "PB3,PB2");
+ #define GPIO_Rx GPIO('B', 3)
+ #define GPIO_Tx GPIO('B', 2)
+ #define CANx_GCLK_ID MCAN0_CLOCK_ID
+#elif CONFIG_ATSAM_CANBUS_PC12_PD12
+ DECL_CONSTANT_STR("RESERVE_PINS_CAN", "PC12,PD12");
+ #define GPIO_Rx GPIO('C', 12)
+ #define GPIO_Tx GPIO('D', 12)
+ #define CANx_GCLK_ID MCAN1_CLOCK_ID
+#endif
+
+#if CANx_GCLK_ID == MCAN0_CLOCK_ID
+ #define CAN_FUNCTION_Rx 'A'
+ #define CAN_FUNCTION_Tx 'A'
+ #define CANx MCAN0
+ #define CANx_IRQn MCAN0_INT0_IRQn
+ #define CCFG_CANxDMABA MATRIX->CCFG_CAN0
+ #define CCFG_CANxDMABA_Msk CCFG_CAN0_CAN0DMABA_Msk
+ #define CCFG_CANxDMABA_Pos CCFG_CAN0_CAN0DMABA_Pos
+#else
+ #define CAN_FUNCTION_Rx 'C'
+ #define CAN_FUNCTION_Tx 'B'
+ #define CANx MCAN1
+ #define CANx_IRQn MCAN1_INT0_IRQn
+ #define CCFG_CANxDMABA MATRIX->CCFG_SYSIO
+ #define CCFG_CANxDMABA_Msk CCFG_SYSIO_CAN1DMABA_Msk
+ #define CCFG_CANxDMABA_Pos CCFG_SYSIO_CAN1DMABA_Pos
+#endif
+
+
+/****************************************************************
+ * Message ram layout
+ ****************************************************************/
+
+struct fdcan_fifo {
+    uint32_t id_section;
+    uint32_t dlc_section;
+    uint32_t data[64 / 4];
+};
+
+#define FDCAN_XTD (1<<30)
+#define FDCAN_RTR (1<<29)
+
+struct fdcan_msg_ram {
+    uint32_t FLS[28]; // Filter list standard
+    uint32_t FLE[16]; // Filter list extended
+    struct fdcan_fifo RXF0[3];
+    struct fdcan_fifo RXF1[3];
+    uint32_t TEF[6]; // Tx event FIFO
+    struct fdcan_fifo TXFIFO[3];
+};
+
+// Message ram is in regular memory
+static struct fdcan_msg_ram MSG_RAM;
+
+
+/****************************************************************
+ * CANbus code
+ ****************************************************************/
+
+#define FDCAN_IE_TC        (MCAN_IE_TCE | MCAN_IE_TCFE | MCAN_IE_TFEE)
+
+// Transmit a packet
+int
+canhw_send(struct canbus_msg *msg)
+{
+    uint32_t txfqs = CANx->MCAN_TXFQS;
+    if (txfqs & MCAN_TXFQS_TFQF)
+        // No space in transmit fifo - wait for irq
+        return -1;
+
+    uint32_t w_index = ((txfqs & MCAN_TXFQS_TFQPI_Msk) >> MCAN_TXFQS_TFQPI_Pos);
+    struct fdcan_fifo *txfifo = &MSG_RAM.TXFIFO[w_index];
+    uint32_t ids;
+    if (msg->id & CANMSG_ID_EFF)
+        ids = (msg->id & 0x1fffffff) | FDCAN_XTD;
+    else
+        ids = (msg->id & 0x7ff) << 18;
+    ids |= msg->id & CANMSG_ID_RTR ? FDCAN_RTR : 0;
+    txfifo->id_section = ids;
+    txfifo->dlc_section = (msg->dlc & 0x0f) << 16;
+    txfifo->data[0] = msg->data32[0];
+    txfifo->data[1] = msg->data32[1];
+    __DMB();
+    CANx->MCAN_TXBAR;
+    CANx->MCAN_TXBAR = ((uint32_t)1 << w_index);
+    return CANMSG_DATA_LEN(msg);
+}
+
+static void
+can_filter(uint32_t index, uint32_t id)
+{
+    MSG_RAM.FLS[index] = ((0x2 << 30) // Classic filter
+                          | (0x1 << 27) // Store in Rx FIFO 0 if filter matches
+                          | (id << 16)
+                          | 0x7FF); // mask all enabled
+}
+
+// Setup the receive packet filter
+void
+canhw_set_filter(uint32_t id)
+{
+    if (!CONFIG_CANBUS_FILTER)
+        return;
+    /* Request initialisation */
+    CANx->MCAN_CCCR |= MCAN_CCCR_INIT;
+    /* Wait the acknowledge */
+    while (!(CANx->MCAN_CCCR & MCAN_CCCR_INIT))
+        ;
+    /* Enable configuration change */
+    CANx->MCAN_CCCR |= MCAN_CCCR_CCE;
+
+    // Load filter
+    can_filter(0, CANBUS_ID_ADMIN);
+    can_filter(1, id);
+    can_filter(2, id + 1);
+
+    uint32_t flssa = (uint32_t)MSG_RAM.FLS
+        - (CCFG_CANxDMABA_Msk & CCFG_CANxDMABA);
+    CANx->MCAN_SIDFC = flssa | ((id ? 3 : 1) << MCAN_SIDFC_LSS_Pos);
+    CANx->MCAN_GFC = 0x02 << MCAN_GFC_ANFS_Pos;
+
+    /* Leave the initialisation mode for the filter */
+    barrier();
+    CANx->MCAN_CCCR &= ~MCAN_CCCR_CCE;
+    CANx->MCAN_CCCR &= ~MCAN_CCCR_INIT;
+}
+
+// This function handles CAN global interrupts
+void
+CAN_IRQHandler(void)
+{
+    uint32_t ir = CANx->MCAN_IR;
+
+    if (ir & MCAN_IE_RF0NE) {
+        CANx->MCAN_IR = MCAN_IE_RF0NE;
+
+        uint32_t rxf0s = CANx->MCAN_RXF0S;
+        if (rxf0s & MCAN_RXF0S_F0FL_Msk) {
+            // Read and ack data packet
+            uint32_t idx = (rxf0s & MCAN_RXF0S_F0GI_Msk) >> MCAN_RXF0S_F0GI_Pos;
+            struct fdcan_fifo *rxf0 = &MSG_RAM.RXF0[idx];
+            uint32_t ids = rxf0->id_section;
+            struct canbus_msg msg;
+            if (ids & FDCAN_XTD)
+                msg.id = (ids & 0x1fffffff) | CANMSG_ID_EFF;
+            else
+                msg.id = (ids >> 18) & 0x7ff;
+            msg.id |= ids & FDCAN_RTR ? CANMSG_ID_RTR : 0;
+            msg.dlc = (rxf0->dlc_section >> 16) & 0x0f;
+            msg.data32[0] = rxf0->data[0];
+            msg.data32[1] = rxf0->data[1];
+            barrier();
+            CANx->MCAN_RXF0A = idx;
+
+            // Process packet
+            canbus_process_data(&msg);
+        }
+    }
+    if (ir & FDCAN_IE_TC) {
+        // Tx
+        CANx->MCAN_IR = FDCAN_IE_TC;
+        canbus_notify_tx();
+    }
+}
+
+static inline const uint32_t
+make_btr(uint32_t sjw,       // Sync jump width, ... hmm
+         uint32_t time_seg1, // time segment before sample point, 1 .. 16
+         uint32_t time_seg2, // time segment after sample point, 1 .. 8
+         uint32_t brp)       // Baud rate prescaler, 1 .. 1024
+{
+    return (((uint32_t)(sjw-1)) << MCAN_NBTP_NSJW_Pos
+            | ((uint32_t)(time_seg1-1)) << MCAN_NBTP_NTSEG1_Pos
+            | ((uint32_t)(time_seg2-1)) << MCAN_NBTP_NTSEG2_Pos
+            | ((uint32_t)(brp - 1)) << MCAN_NBTP_NBRP_Pos);
+}
+
+static inline const uint32_t
+compute_btr(uint32_t pclock, uint32_t bitrate)
+{
+    /*
+        Some equations:
+        Tpclock = 1 / pclock
+        Tq      = brp * Tpclock
+        Tbs1    = Tq * TS1
+        Tbs2    = Tq * TS2
+        NominalBitTime = Tq + Tbs1 + Tbs2
+        BaudRate = 1/NominalBitTime
+        Bit value sample point is after Tq+Tbs1. Ideal sample point
+        is at 87.5% of NominalBitTime
+        Use the lowest brp where ts1 and ts2 are in valid range
+     */
+
+    uint32_t bit_clocks = pclock / bitrate; // clock ticks per bit
+
+    uint32_t sjw = 2;
+    uint32_t qs;
+    // Find number of time quantas that gives us the exact wanted bit time
+    for (qs = 18; qs > 9; qs--) {
+        // check that bit_clocks / quantas is an integer
+        uint32_t brp_rem = bit_clocks % qs;
+        if (brp_rem == 0)
+            break;
+    }
+    uint32_t brp       = bit_clocks / qs;
+    uint32_t time_seg2 = qs / 8; // sample at ~87.5%
+    uint32_t time_seg1 = qs - (1 + time_seg2);
+
+    return make_btr(sjw, time_seg1, time_seg2, brp);
+}
+
+void
+can_init(void)
+{
+    if (!CONFIG_USBCANBUS) {
+        // Configure UPLL for USB and CAN
+        PMC->CKGR_UCKR = CKGR_UCKR_UPLLCOUNT(3) | CKGR_UCKR_UPLLEN;
+        while (!(PMC->PMC_SR & PMC_SR_LOCKU))
+            ;
+    }
+
+    // Configure PCK5 for CAN use
+    PMC->PMC_PCK[5] = PMC_PCK_CSS_UPLL_CLK | PMC_PCK_PRES(5);
+    while (!(PMC->PMC_SR & PMC_SR_PCKRDY5))
+        ;
+    PMC->PMC_SCER |= PMC_SCER_PCK5;
+
+    enable_pclock(CANx_GCLK_ID);
+
+    gpio_peripheral(GPIO_Rx, CAN_FUNCTION_Rx, 1);
+    gpio_peripheral(GPIO_Tx, CAN_FUNCTION_Tx, 0);
+
+    uint32_t pclock = get_pclock_frequency(CANx_GCLK_ID);
+
+    uint32_t btr = compute_btr(pclock, CONFIG_CANBUS_FREQUENCY);
+
+    /*##-1- Configure the CAN #######################################*/
+
+    /* Exit from sleep mode */
+    CANx->MCAN_CCCR &= ~MCAN_CCCR_CSR;
+    /* Wait the acknowledge */
+    while (CANx->MCAN_CCCR & MCAN_CCCR_CSA)
+        ;
+    /* Request initialization */
+    CANx->MCAN_CCCR |= MCAN_CCCR_INIT;
+    /* Wait the acknowledge */
+    while (!(CANx->MCAN_CCCR & MCAN_CCCR_INIT))
+        ;
+    /* Enable configuration change */
+    CANx->MCAN_CCCR |= MCAN_CCCR_CCE;
+
+    /* Disable protocol exception handling */
+    CANx->MCAN_CCCR |= MCAN_CCCR_PXHD;
+
+    CANx->MCAN_NBTP = btr;
+
+    /* Setup message RAM addresses */
+    uint32_t ccfg = (CCFG_CANxDMABA & ~CCFG_CANxDMABA_Msk);
+    CCFG_CANxDMABA = (ccfg | (((uint32_t)&MSG_RAM)
+        & CCFG_CANxDMABA_Msk));
+    uint32_t f0sa = (uint32_t)MSG_RAM.RXF0
+        - (CCFG_CANxDMABA_Msk & CCFG_CANxDMABA);
+    CANx->MCAN_RXF0C = f0sa
+        | (ARRAY_SIZE(MSG_RAM.RXF0) << MCAN_RXF0C_F0S_Pos);
+    CANx->MCAN_RXESC = (7 << MCAN_RXESC_F1DS_Pos)
+        | (7 << MCAN_RXESC_F0DS_Pos);
+    uint32_t tbsa = (uint32_t)MSG_RAM.TXFIFO
+        - (CCFG_CANxDMABA_Msk & CCFG_CANxDMABA);
+    CANx->MCAN_TXBC = tbsa
+        | (ARRAY_SIZE(MSG_RAM.TXFIFO) << MCAN_TXBC_TFQS_Pos);
+    CANx->MCAN_TXESC = 7 << MCAN_TXESC_TBDS_Pos;
+
+    /* Leave the initialisation mode */
+    CANx->MCAN_CCCR &= ~MCAN_CCCR_CCE;
+    CANx->MCAN_CCCR &= ~MCAN_CCCR_INIT;
+
+    /*##-2- Configure the CAN Filter #######################################*/
+    canhw_set_filter(0);
+
+    /*##-3- Configure Interrupts #################################*/
+    armcm_enable_irq(CAN_IRQHandler, CANx_IRQn, 1);
+    CANx->MCAN_ILE = MCAN_ILE_EINT0;
+    CANx->MCAN_IE = MCAN_IE_RF0NE | FDCAN_IE_TC;
+}
+DECL_INIT(can_init);
diff --git a/src/atsam/main.c b/src/atsam/main.c
index 8c2e4318d..3728b7349 100644
--- a/src/atsam/main.c
+++ b/src/atsam/main.c
@@ -14,6 +14,8 @@
 #define FREQ_PERIPH_DIV (CONFIG_MACH_SAME70 ? 2 : 1)
 #define FREQ_PERIPH (CONFIG_CLOCK_FREQ / FREQ_PERIPH_DIV)
 
+#define FREQ_SAME70_CAN 80000000
+
 /****************************************************************
  * watchdog handler
  ****************************************************************/
@@ -62,6 +64,10 @@ enable_pclock(uint32_t id)
 uint32_t
 get_pclock_frequency(uint32_t id)
 {
+#if CONFIG_MACH_SAME70
+    if (id == MCAN0_CLOCK_ID || id == MCAN1_CLOCK_ID)
+        return FREQ_SAME70_CAN;
+#endif
     return FREQ_PERIPH;
 }
 
diff --git a/src/atsam/same70_sysinit.c b/src/atsam/same70_sysinit.c
index d4d98483f..4cb5f48cc 100644
--- a/src/atsam/same70_sysinit.c
+++ b/src/atsam/same70_sysinit.c
@@ -70,5 +70,8 @@ void SystemInit( void )
 
     // Configure PCK6 for TC use
     PMC->PMC_PCK[6] = PMC_PCK_CSS_MCK | PMC_PCK_PRES(2);
-    PMC->PMC_SCER = PMC_SCER_PCK6;
+    while ( !(PMC->PMC_SR & PMC_SR_PCKRDY6) )
+    {
+    }
+    PMC->PMC_SCER |= PMC_SCER_PCK6;
 }