From 21b784297916ac31b0e83161976effea048b53cb Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 26 Sep 2023 16:30:58 -0400 Subject: [PATCH 01/21] docs: Add Peopoly to Sponsors.md Signed-off-by: Kevin O'Connor --- docs/Sponsors.md | 5 +++-- docs/img/sponsors/peopoly-logo.png | Bin 0 -> 67472 bytes 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 docs/img/sponsors/peopoly-logo.png diff --git a/docs/Sponsors.md b/docs/Sponsors.md index 7376cb31ca62..a226bb57bb5c 100644 --- a/docs/Sponsors.md +++ b/docs/Sponsors.md @@ -6,7 +6,7 @@ sponsors. ## BIGTREETECH -[](https://bigtree-tech.com/collections/all-products) +[](https://bigtree-tech.com/collections/all-products) BIGTREETECH is the official mainboard sponsor of Klipper. BIGTREETECH is committed to developing innovative and competitive products to @@ -16,7 +16,8 @@ serve the 3D printing community better. Follow them on ## Sponsors -[](https://obico.io/klipper.html?source=klipper_sponsor) +[](https://obico.io/klipper.html?source=klipper_sponsor) +[](https://peopoly.net) ## Klipper Developers diff --git a/docs/img/sponsors/peopoly-logo.png b/docs/img/sponsors/peopoly-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bcf1bd9ce61301b7f244b57f2ea7c73089639b8c GIT binary patch literal 67472 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 LSgYtf>W}{g&O_S_ literal 0 HcmV?d00001 From a4cd0336bdd9df78c1b2a1296a481e84efc2b821 Mon Sep 17 00:00:00 2001 From: Dmitry Butyugin Date: Sat, 30 Sep 2023 02:46:42 +0900 Subject: [PATCH 02/21] idex_modes: Fixed the case when carriages home in the same direction (#6310) Previous version of the code assumed that dual carriages home away from each other, which is not true on some machines, which have the second dual carriage homing on the first carriage. The new code correctly identifies the relative order of the carriages now. This fixes discrepancies between the documentation and the actual implementation of the carriages kinematic ranges calculation. Notes about dual_carriage homing and proximity checks changes Fixed clearing of homing state after homing in certain modes In case of multi-MCU homing it is possible that the carriage position will end up outside of the allowed motion range due to latencies in data transmission between MCUs. Selecting certain modes after homing could result in home state clearing instead of blocking the motion of the active carriage. This commit fixes this undesired behavior. Signed-off-by: Dmitry Butyugin --- docs/Config_Changes.md | 9 ++++++ docs/Config_Reference.md | 8 +++++ klippy/kinematics/idex_modes.py | 53 ++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/docs/Config_Changes.md b/docs/Config_Changes.md index 063af8ac390f..6d83238d0295 100644 --- a/docs/Config_Changes.md +++ b/docs/Config_Changes.md @@ -8,6 +8,15 @@ All dates in this document are approximate. ## Changes +20230826: If `safe_distance` is set or calculated to be 0 in `[dual_carriage]`, +the carriages proximity checks will be disabled as per documentation. A user +may wish to configure `safe_distance` explicitly to prevent accidental crashes +of the carriages with each other. Additionally, the homing order of the primary +and the dual carriage is changed in some configurations (certain configurations +when both carriages home in the same direction, see +[[dual_carriage] configuration reference](./Config_Reference.md#dual_carriage) +for more details). + 20230810: The flash-sdcard.sh script now supports both variants of the Bigtreetech SKR-3, STM32H743 and STM32H723. For this, the original tag of btt-skr-3 now has changed to be either btt-skr-3-h743 or btt-skr-3-h723. diff --git a/docs/Config_Reference.md b/docs/Config_Reference.md index 25e7cc46e6fb..241391834e0f 100644 --- a/docs/Config_Reference.md +++ b/docs/Config_Reference.md @@ -2065,6 +2065,14 @@ in this section (CARRIAGE=0 will return activation to the primary carriage). Dual carriage support is typically combined with extra extruders - the SET_DUAL_CARRIAGE command is often called at the same time as the ACTIVATE_EXTRUDER command. Be sure to park the carriages during deactivation. +Note that during G28 homing, typically the primary carriage is homed first +followed by the carriage defined in the `[dual_carriage]` config section. +However, the `[dual_carriage]` carriage will be homed first if both carriages +home in a positive direction and the [dual_carriage] carriage has a +`position_endstop` greater than the primary carriage, or if both carriages home +in a negative direction and the `[dual_carriage]` carriage has a +`position_endstop` less than the primary carriage. + Additionally, one could use "SET_DUAL_CARRIAGE CARRIAGE=1 MODE=COPY" or "SET_DUAL_CARRIAGE CARRIAGE=1 MODE=MIRROR" commands to activate either copying or mirroring mode of the dual carriage, in which case it will follow the diff --git a/klippy/kinematics/idex_modes.py b/klippy/kinematics/idex_modes.py index b54f96579c9b..2ce91afe85a5 100644 --- a/klippy/kinematics/idex_modes.py +++ b/klippy/kinematics/idex_modes.py @@ -65,7 +65,13 @@ def toggle_active_dc_rail(self, index, override_rail=False): kin.update_limits(self.axis, target_dc.get_rail().get_range()) def home(self, homing_state): kin = self.printer.lookup_object('toolhead').get_kinematics() - for i, dc_rail in enumerate(self.dc): + enumerated_dcs = list(enumerate(self.dc)) + if (self.get_dc_order(0, 1) > 0) != \ + self.dc[0].get_rail().get_homing_info().positive_dir: + # The second carriage must home first, because the carriages home in + # the same direction and the first carriage homes on the second one + enumerated_dcs.reverse() + for i, dc_rail in enumerated_dcs: self.toggle_active_dc_rail(i, override_rail=True) kin.home_axis(homing_state, self.axis, dc_rail.get_rail()) # Restore the original rails ordering @@ -78,9 +84,15 @@ def get_kin_range(self, toolhead, mode): axes_pos = [dc.get_axis_position(pos) for dc in self.dc] dc0_rail = self.dc[0].get_rail() dc1_rail = self.dc[1].get_rail() - range_min = dc0_rail.position_min - range_max = dc0_rail.position_max + if mode != PRIMARY or self.dc[0].is_active(): + range_min = dc0_rail.position_min + range_max = dc0_rail.position_max + else: + range_min = dc1_rail.position_min + range_max = dc1_rail.position_max safe_dist = self.safe_dist + if not safe_dist: + return (range_min, range_max) if mode == COPY: range_min = max(range_min, @@ -88,7 +100,7 @@ def get_kin_range(self, toolhead, mode): range_max = min(range_max, axes_pos[0] - axes_pos[1] + dc1_rail.position_max) elif mode == MIRROR: - if dc0_rail.get_homing_info().positive_dir: + if self.get_dc_order(0, 1) > 0: range_min = max(range_min, 0.5 * (sum(axes_pos) + safe_dist)) range_max = min(range_max, @@ -102,14 +114,39 @@ def get_kin_range(self, toolhead, mode): # mode == PRIMARY active_idx = 1 if self.dc[1].is_active() else 0 inactive_idx = 1 - active_idx - if active_idx: - range_min = dc1_rail.position_min - range_max = dc1_rail.position_max - if self.dc[active_idx].get_rail().get_homing_info().positive_dir: + if self.get_dc_order(active_idx, inactive_idx) > 0: range_min = max(range_min, axes_pos[inactive_idx] + safe_dist) else: range_max = min(range_max, axes_pos[inactive_idx] - safe_dist) + if range_min > range_max: + # During multi-MCU homing it is possible that the carriage + # position will end up below position_min or above position_max + # if position_endstop is too close to the rail motion ends due + # to inherent latencies of the data transmission between MCUs. + # This can result in an invalid range_min > range_max range + # in certain modes, which may confuse the kinematics code. + # So, return an empty range instead, which will correctly + # block the carriage motion until a different mode is selected + # which actually permits carriage motion. + return (range_min, range_min) return (range_min, range_max) + def get_dc_order(self, first, second): + if first == second: + return 0 + # Check the relative order of the first and second carriages and + # return -1 if the first carriage position is always smaller + # than the second one and 1 otherwise + first_rail = self.dc[first].get_rail() + second_rail = self.dc[second].get_rail() + first_homing_info = first_rail.get_homing_info() + second_homing_info = second_rail.get_homing_info() + if first_homing_info.positive_dir != second_homing_info.positive_dir: + # Carriages home away from each other + return 1 if first_homing_info.positive_dir else -1 + # Carriages home in the same direction + if first_rail.position_endstop > second_rail.position_endstop: + return 1 + return -1 def activate_dc_mode(self, index, mode): toolhead = self.printer.lookup_object('toolhead') toolhead.flush_step_generation() From 7bd32994d4ee7eff613413d7a813bb3b17b8f6d3 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Mon, 18 Sep 2023 22:23:35 +0900 Subject: [PATCH 03/21] docs: fix typo in RPi_microcontroller.md additionaly -> additionally Signed-off-by: Ikko Eltociear Ashimine --- docs/RPi_microcontroller.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/RPi_microcontroller.md b/docs/RPi_microcontroller.md index 4e3c057fe41b..96ac5626f9a0 100644 --- a/docs/RPi_microcontroller.md +++ b/docs/RPi_microcontroller.md @@ -203,7 +203,7 @@ channels need to be enabled you can use `pwm-2chan`: # Enable pwmchip sysfs interface dtoverlay=pwm-2chan,pin=12,func=4,pin2=13,func2=4 ``` -This example additionaly enables PWM1 and routes it to gpio13. +This example additionally enables PWM1 and routes it to gpio13. The overlay does not expose the pwm line on sysfs on boot and needs to be exported by echo'ing the number of the pwm channel to From aa726cb7cbf6c58607885efe8f0cb07d009d5546 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 3 Oct 2023 23:24:18 -0400 Subject: [PATCH 04/21] lib: Update to latest can2040 code Add support for can2040_stop() Add data_state_go_error() helper Add new can2040_get_statistics() API function Call report_note_discarding() after setting MS_DISCARD state Convert report_is_rx_eof_pending() to report_is_not_in_tx() Signed-off-by: Kevin O'Connor --- lib/README | 2 +- lib/can2040/can2040.c | 97 ++++++++++++++++++++++++++++++++----------- lib/can2040/can2040.h | 10 ++++- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/lib/README b/lib/README index b3bd95e563ce..e981df59f78d 100644 --- a/lib/README +++ b/lib/README @@ -167,7 +167,7 @@ used to upload firmware to devices flashed with the CanBoot bootloader. The can2040 directory contains code from: https://github.com/KevinOConnor/can2040 -revision d1190afcaa6245c20da28199d06e453d2e743099. +version v1.6.0 (af3d21e5d61b8408c63fbdfb0aceb21d69d91693) The Huada HC32F460 directory contains code from: https://www.hdsc.com.cn/Category83-1490 diff --git a/lib/can2040/can2040.c b/lib/can2040/can2040.c index 926893d94c0a..c2bd0061a57c 100644 --- a/lib/can2040/can2040.c +++ b/lib/can2040/can2040.c @@ -1,6 +1,6 @@ // Software CANbus implementation for rp2040 // -// Copyright (C) 2022 Kevin O'Connor +// Copyright (C) 2022,2023 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -318,6 +318,14 @@ pio_irq_set(struct can2040 *cd, uint32_t sm_irqs) pio_hw->inte0 = sm_irqs | SI_RX_DATA; } +// Completely disable host irqs +static void +pio_irq_disable(struct can2040 *cd) +{ + pio_hw_t *pio_hw = cd->pio_hw; + pio_hw->inte0 = 0; +} + // Return current host irq mask static uint32_t pio_irq_get(struct can2040 *cd) @@ -662,6 +670,7 @@ tx_schedule_transmit(struct can2040 *cd) pio_signal_set_txpending(cd); } cd->tx_state = TS_QUEUED; + cd->stats.tx_attempt++; struct can2040_transmit *qt = &cd->tx_queue[tx_qpos(cd, tx_pull_pos)]; pio_tx_send(cd, qt->stuffed_data, qt->stuffed_words); return 0; @@ -721,6 +730,7 @@ report_callback_error(struct can2040 *cd, uint32_t error_code) static void report_callback_rx_msg(struct can2040 *cd) { + cd->stats.rx_total++; cd->rx_cb(cd, CAN2040_NOTIFY_RX, &cd->parse_msg); } @@ -729,6 +739,7 @@ static void report_callback_tx_msg(struct can2040 *cd) { writel(&cd->tx_pull_pos, cd->tx_pull_pos + 1); + cd->stats.tx_total++; cd->rx_cb(cd, CAN2040_NOTIFY_TX, &cd->parse_msg); } @@ -748,11 +759,11 @@ report_handle_eof(struct can2040 *cd) pio_match_clear(cd); } -// Check if in an rx message is being processed +// Check if message being processed is an rx message (not self feedback from tx) static int -report_is_rx_eof_pending(struct can2040 *cd) +report_is_not_in_tx(struct can2040 *cd) { - return cd->report_state == RS_NEED_RX_EOF; + return !(cd->report_state & RS_NEED_TX_ACK); } // Parser found a new message start @@ -817,7 +828,7 @@ report_note_eof_success(struct can2040 *cd) // Parser found unexpected data on input static void -report_note_parse_error(struct can2040 *cd) +report_note_discarding(struct can2040 *cd) { if (cd->report_state != RS_IDLE) { cd->report_state = RS_IDLE; @@ -880,7 +891,7 @@ report_line_txpending(struct can2040 *cd) return; } // Tx request from can2040_transmit(), report_note_eof_success(), - // or report_note_parse_error(). + // or report_note_discarding(). uint32_t check_txpending = tx_schedule_transmit(cd); pio_irq_set(cd, (pio_irqs & ~SI_TXPENDING) | check_txpending); } @@ -896,6 +907,13 @@ enum { MS_CRC, MS_ACK, MS_EOF0, MS_EOF1, MS_DISCARD }; +// Reset any bits in the incoming parsing state +static void +data_state_clear_bits(struct can2040 *cd) +{ + cd->raw_bit_count = cd->unstuf.stuffed_bits = cd->unstuf.count_stuff = 0; +} + // Transition to the next parsing state static void data_state_go_next(struct can2040 *cd, uint32_t state, uint32_t num_bits) @@ -908,23 +926,35 @@ data_state_go_next(struct can2040 *cd, uint32_t state, uint32_t num_bits) static void data_state_go_discard(struct can2040 *cd) { - report_note_parse_error(cd); - if (pio_rx_check_stall(cd)) { // CPU couldn't keep up for some read data - must reset pio state - cd->raw_bit_count = cd->unstuf.count_stuff = 0; + data_state_clear_bits(cd); pio_sm_setup(cd); report_callback_error(cd, 0); } data_state_go_next(cd, MS_DISCARD, 32); + + // Clear report state and update hw irqs after transition to MS_DISCARD + report_note_discarding(cd); +} + +// Note a data parse error and transition to discard state +static void +data_state_go_error(struct can2040 *cd) +{ + cd->stats.parse_error++; + data_state_go_discard(cd); } // Received six dominant bits on the line static void data_state_line_error(struct can2040 *cd) { - data_state_go_discard(cd); + if (cd->parse_state == MS_DISCARD) + data_state_go_discard(cd); + else + data_state_go_error(cd); } // Received six unexpected passive bits on the line @@ -933,7 +963,7 @@ data_state_line_passive(struct can2040 *cd) { if (cd->parse_state != MS_DISCARD && cd->parse_state != MS_START) { // Bitstuff error - data_state_go_discard(cd); + data_state_go_error(cd); return; } @@ -941,8 +971,7 @@ data_state_line_passive(struct can2040 *cd) uint32_t dom_bits = ~stuffed_bits; if (!dom_bits) { // Counter overflow in "sync" state machine - reset it - cd->unstuf.stuffed_bits = 0; - cd->raw_bit_count = cd->unstuf.count_stuff = 0; + data_state_clear_bits(cd); pio_sm_setup(cd); data_state_go_discard(cd); return; @@ -972,7 +1001,7 @@ data_state_go_crc(struct can2040 *cd) int ret = report_note_crc_start(cd); if (ret) { - data_state_go_discard(cd); + data_state_go_error(cd); return; } data_state_go_next(cd, MS_CRC, 16); @@ -1065,7 +1094,7 @@ static void data_state_update_crc(struct can2040 *cd, uint32_t data) { if (((cd->parse_crc << 1) | 1) != data) { - data_state_go_discard(cd); + data_state_go_error(cd); return; } @@ -1083,7 +1112,7 @@ data_state_update_ack(struct can2040 *cd, uint32_t data) // data_state_line_passive() unstuf_restore_state(&cd->unstuf, (cd->parse_crc_bits << 2) | data); - data_state_go_discard(cd); + data_state_go_error(cd); return; } report_note_ack_success(cd); @@ -1095,7 +1124,7 @@ static void data_state_update_eof0(struct can2040 *cd, uint32_t data) { if (data != 0x0f || pio_rx_check_stall(cd)) { - data_state_go_discard(cd); + data_state_go_error(cd); return; } unstuf_clear_state(&cd->unstuf); @@ -1106,14 +1135,17 @@ data_state_update_eof0(struct can2040 *cd, uint32_t data) static void data_state_update_eof1(struct can2040 *cd, uint32_t data) { - if (data >= 0x1c || (data >= 0x18 && report_is_rx_eof_pending(cd))) - // Message is considered fully transmitted + if (data == 0x1f) { + // Success report_note_eof_success(cd); - - if (data == 0x1f) data_state_go_next(cd, MS_START, 1); - else + } else if (data >= 0x1c || (data >= 0x18 && report_is_not_in_tx(cd))) { + // Message fully transmitted - followed by "overload frame" + report_note_eof_success(cd); data_state_go_discard(cd); + } else { + data_state_go_error(cd); + } } // Handle data received while in MS_DISCARD state @@ -1310,13 +1342,28 @@ can2040_start(struct can2040 *cd, uint32_t sys_clock, uint32_t bitrate { cd->gpio_rx = gpio_rx; cd->gpio_tx = gpio_tx; + data_state_clear_bits(cd); pio_setup(cd, sys_clock, bitrate); data_state_go_discard(cd); } -// API function to stop and uninitialize can2040 code +// API function to stop can2040 code void -can2040_shutdown(struct can2040 *cd) +can2040_stop(struct can2040 *cd) { - // XXX + pio_irq_disable(cd); + pio_sm_setup(cd); +} + +// API function to access can2040 statistics +void +can2040_get_statistics(struct can2040 *cd, struct can2040_stats *stats) +{ + for (;;) { + memcpy(stats, &cd->stats, sizeof(*stats)); + if (memcmp(stats, &cd->stats, sizeof(*stats)) == 0) + // Successfully copied data + return; + // Raced with irq handler update - retry copy + } } diff --git a/lib/can2040/can2040.h b/lib/can2040/can2040.h index fc0bdd62784c..7dbee1162555 100644 --- a/lib/can2040/can2040.h +++ b/lib/can2040/can2040.h @@ -26,11 +26,18 @@ struct can2040; typedef void (*can2040_rx_cb)(struct can2040 *cd, uint32_t notify , struct can2040_msg *msg); +struct can2040_stats { + uint32_t rx_total, tx_total; + uint32_t tx_attempt; + uint32_t parse_error; +}; + void can2040_setup(struct can2040 *cd, uint32_t pio_num); void can2040_callback_config(struct can2040 *cd, can2040_rx_cb rx_cb); void can2040_start(struct can2040 *cd, uint32_t sys_clock, uint32_t bitrate , uint32_t gpio_rx, uint32_t gpio_tx); -void can2040_shutdown(struct can2040 *cd); +void can2040_stop(struct can2040 *cd); +void can2040_get_statistics(struct can2040 *cd, struct can2040_stats *stats); void can2040_pio_irq_handler(struct can2040 *cd); int can2040_check_transmit(struct can2040 *cd); int can2040_transmit(struct can2040 *cd, struct can2040_msg *msg); @@ -56,6 +63,7 @@ struct can2040 { void *pio_hw; uint32_t gpio_rx, gpio_tx; can2040_rx_cb rx_cb; + struct can2040_stats stats; // Bit unstuffing struct can2040_bitunstuffer unstuf; From 1e3ace2170b2b610c3e08d452aefe72f92bf898c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 23 Sep 2023 12:26:21 -0400 Subject: [PATCH 05/21] stm32: Improve usbfs epr register handling Replace the set_stat_x_bits() functions with a single calc_epr_bits() function. This new function supports setting bits other than the stat field in the epr register. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 81 ++++++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index fda2ce9d299c..ffdfb07b0634 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -1,6 +1,6 @@ // Hardware interface to "fullspeed USB controller" // -// Copyright (C) 2018-2021 Kevin O'Connor +// Copyright (C) 2018-2023 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -175,26 +175,19 @@ btable_write_packet(int ep, const uint8_t *src, int count) #define USB_EPR ((volatile uint32_t *)USB_BASE) +#define EPR_TBITS (USB_EP_DTOG_RX | USB_EPRX_STAT \ + | USB_EP_DTOG_TX | USB_EPTX_STAT) #define EPR_RWBITS (USB_EPADDR_FIELD | USB_EP_KIND | USB_EP_TYPE_MASK) #define EPR_RWCBITS (USB_EP_CTR_RX | USB_EP_CTR_TX) +// Calculate the memory update needed to set the epr register static uint32_t -set_stat_rx_bits(uint32_t epr, uint32_t bits) +calc_epr_bits(uint32_t epr, uint32_t mask, uint32_t value) { - return ((epr & (EPR_RWBITS | USB_EPRX_STAT)) ^ bits) | EPR_RWCBITS; -} - -static uint32_t -set_stat_tx_bits(uint32_t epr, uint32_t bits) -{ - return ((epr & (EPR_RWBITS | USB_EPTX_STAT)) ^ bits) | EPR_RWCBITS; -} - -static uint32_t -set_stat_rxtx_bits(uint32_t epr, uint32_t bits) -{ - uint32_t mask = EPR_RWBITS | USB_EPRX_STAT | USB_EPTX_STAT; - return ((epr & mask) ^ bits) | EPR_RWCBITS; + uint32_t tmask = mask & EPR_TBITS, tvalue = value & tmask; + uint32_t rwmask = mask & EPR_RWBITS, rwbits = value & rwmask; + uint32_t cbits = EPR_RWCBITS & ~mask; + return (((epr & (EPR_RWBITS | tmask)) ^ tvalue) & ~rwmask) | rwbits | cbits; } @@ -205,36 +198,37 @@ set_stat_rxtx_bits(uint32_t epr, uint32_t bits) int_fast8_t usb_read_bulk_out(void *data, uint_fast8_t max_len) { - uint32_t epr = USB_EPR[USB_CDC_EP_BULK_OUT]; + uint32_t ep = USB_CDC_EP_BULK_OUT, epr = USB_EPR[ep]; if ((epr & USB_EPRX_STAT) == USB_EP_RX_VALID) // No data ready return -1; - uint32_t count = btable_read_packet(USB_CDC_EP_BULK_OUT, data, max_len); - USB_EPR[USB_CDC_EP_BULK_OUT] = set_stat_rx_bits(epr, USB_EP_RX_VALID); + uint32_t count = btable_read_packet(ep, data, max_len); + USB_EPR[ep] = calc_epr_bits(epr, USB_EPRX_STAT, USB_EP_RX_VALID); return count; } int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) { - uint32_t epr = USB_EPR[USB_CDC_EP_BULK_IN]; + uint32_t ep = USB_CDC_EP_BULK_IN, epr = USB_EPR[ep]; if ((epr & USB_EPTX_STAT) != USB_EP_TX_NAK) // No buffer space available return -1; - btable_write_packet(USB_CDC_EP_BULK_IN, data, len); - USB_EPR[USB_CDC_EP_BULK_IN] = set_stat_tx_bits(epr, USB_EP_TX_VALID); + btable_write_packet(ep, data, len); + USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT, USB_EP_TX_VALID); return len; } int_fast8_t usb_read_ep0(void *data, uint_fast8_t max_len) { - uint32_t epr = USB_EPR[0]; + uint32_t ep = 0, epr = USB_EPR[ep]; if ((epr & USB_EPRX_STAT) != USB_EP_RX_NAK) // No data ready return -1; - uint32_t count = btable_read_packet(0, data, max_len); - USB_EPR[0] = set_stat_rxtx_bits(epr, USB_EP_RX_VALID | USB_EP_TX_NAK); + uint32_t count = btable_read_packet(ep, data, max_len); + USB_EPR[ep] = calc_epr_bits(epr, USB_EPRX_STAT | USB_EPTX_STAT + , USB_EP_RX_VALID | USB_EP_TX_NAK); return count; } @@ -247,23 +241,24 @@ usb_read_ep0_setup(void *data, uint_fast8_t max_len) int_fast8_t usb_send_ep0(const void *data, uint_fast8_t len) { - uint32_t epr = USB_EPR[0]; + uint32_t ep = 0, epr = USB_EPR[ep]; if ((epr & USB_EPRX_STAT) != USB_EP_RX_VALID) // Transfer interrupted return -2; if ((epr & USB_EPTX_STAT) != USB_EP_TX_NAK) // No buffer space available return -1; - btable_write_packet(0, data, len); - USB_EPR[0] = set_stat_tx_bits(epr, USB_EP_TX_VALID); + btable_write_packet(ep, data, len); + USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT, USB_EP_TX_VALID); return len; } void usb_stall_ep0(void) { - USB_EPR[0] = set_stat_rxtx_bits(USB_EPR[0] - , USB_EP_RX_STALL | USB_EP_TX_STALL); + uint32_t ep = 0, epr = USB_EPR[ep]; + USB_EPR[ep] = calc_epr_bits(epr, USB_EPRX_STAT | USB_EPTX_STAT + , USB_EP_RX_STALL | USB_EP_TX_STALL); } static uint8_t set_address; @@ -289,13 +284,20 @@ usb_set_configure(void) static void usb_reset(void) { - USB_EPR[0] = 0 | USB_EP_CONTROL | USB_EP_RX_VALID | USB_EP_TX_NAK; - USB_EPR[USB_CDC_EP_ACM] = (USB_CDC_EP_ACM | USB_EP_INTERRUPT - | USB_EP_RX_NAK | USB_EP_TX_NAK); - USB_EPR[USB_CDC_EP_BULK_OUT] = (USB_CDC_EP_BULK_OUT | USB_EP_BULK - | USB_EP_RX_VALID | USB_EP_TX_NAK); - USB_EPR[USB_CDC_EP_BULK_IN] = (USB_CDC_EP_BULK_IN | USB_EP_BULK - | USB_EP_RX_NAK | USB_EP_TX_NAK); + uint32_t ep = 0; + USB_EPR[ep] = 0 | USB_EP_CONTROL | USB_EP_RX_VALID | USB_EP_TX_NAK; + + ep = USB_CDC_EP_ACM; + USB_EPR[ep] = (USB_CDC_EP_ACM | USB_EP_INTERRUPT + | USB_EP_RX_NAK | USB_EP_TX_NAK); + + ep = USB_CDC_EP_BULK_OUT; + USB_EPR[ep] = (USB_CDC_EP_BULK_OUT | USB_EP_BULK + | USB_EP_RX_VALID | USB_EP_TX_NAK); + + ep = USB_CDC_EP_BULK_IN; + USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK + | USB_EP_RX_NAK | USB_EP_TX_NAK); USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM; USB->DADDR = USB_DADDR_EF; @@ -308,9 +310,8 @@ USB_IRQHandler(void) uint32_t istr = USB->ISTR; if (istr & USB_ISTR_CTR) { // Endpoint activity - uint32_t ep = istr & USB_ISTR_EP_ID; - uint32_t epr = USB_EPR[ep]; - USB_EPR[ep] = epr & EPR_RWBITS; + uint32_t ep = istr & USB_ISTR_EP_ID, epr = USB_EPR[ep]; + USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); if (ep == 0) { usb_notify_ep0(); if (epr & USB_EP_CTR_TX && set_address) { From 01ac5334e90d5ba57636e1c56b8e2fee901d4585 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 21 Sep 2023 15:33:37 -0400 Subject: [PATCH 06/21] stm32: Update usbfs to support setting both buffers for each endpoint The usbfs device supports two buffers for each endpoint - typically one for rx and one for tx. Add support for explicit handling of both buffers. This is in preparation for improved "double buffering" support. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 72 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index ffdfb07b0634..3c96fd4ddbce 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -55,52 +55,50 @@ // Layout of the USB transfer memory #define EPM ((epmword_t*)USB_PMAADDR) -#define EPM_EP_DESC(ep) (&EPM[(ep) * (8 / WSIZE)]) +#define EPM_EP_DESC(ep, bufnum) (&EPM[((ep)*2 + (bufnum)) * (4 / WSIZE)]) #define EPM_BUF_OFFSET 0x10 #define EPM_EP_BUF_SIZE (64 / WSIZE + 1) -#define EPM_EP_TX_BUF(ep) (&EPM[EPM_BUF_OFFSET + (ep)*2*EPM_EP_BUF_SIZE]) -#define EPM_EP_RX_BUF(ep) (&EPM[EPM_BUF_OFFSET + (1+(ep)*2)*EPM_EP_BUF_SIZE]) +#define EPM_EP_BUF(ep, bufnum) \ + (&EPM[EPM_BUF_OFFSET + ((ep)*2 + (bufnum)) * EPM_EP_BUF_SIZE]) +#define BUFTX 0 +#define BUFRX 1 // Configure the usb descriptor for an endpoint static void -epm_ep_desc_setup(int ep, int rx_size) +epm_ep_desc_setup(int ep, int bufnum, int rx_size) { - uint32_t addr_tx = (EPM_EP_TX_BUF(ep) - EPM) * WSIZE, count_tx = 0; - uint32_t addr_rx = (EPM_EP_RX_BUF(ep) - EPM) * WSIZE; + uint32_t addr = (EPM_EP_BUF(ep, bufnum) - EPM) * WSIZE; uint32_t count_rx = (rx_size <= 30 ? DIV_ROUND_UP(rx_size, 2) << 10 : ((DIV_ROUND_UP(rx_size, 32) - 1) << 10) | 0x8000); - epmword_t *desc = EPM_EP_DESC(ep); + epmword_t *desc = EPM_EP_DESC(ep, bufnum); if (WSIZE == 2) { - desc[0] = addr_tx; - desc[1] = count_tx; - desc[2] = addr_rx; - desc[3] = count_rx; + desc[0] = addr; + desc[1] = count_rx; } else { - desc[0] = addr_tx | (count_tx << 16); - desc[1] = addr_rx | (count_rx << 16); + *desc = addr | (count_rx << 16); } } // Return number of read bytes on an rx endpoint static uint32_t -epm_get_ep_count_rx(int ep) +epm_get_ep_count_rx(int ep, int bufnum) { - epmword_t *desc = EPM_EP_DESC(ep); + epmword_t *desc = EPM_EP_DESC(ep, bufnum); if (WSIZE == 2) - return desc[3] & 0x3ff; - return (desc[1] >> 16) & 0x3ff; + return desc[1] & 0x3ff; + return (*desc >> 16) & 0x3ff; } // Set number of bytes ready to be transmitted on a tx endpoint static void -epm_set_ep_count_tx(int ep, uint32_t count) +epm_set_ep_count_tx(int ep, int bufnum, uint32_t count) { - epmword_t *desc = EPM_EP_DESC(ep); + epmword_t *desc = EPM_EP_DESC(ep, bufnum); if (WSIZE == 2) { desc[1] = count; } else { - uint32_t addr_tx = (EPM_EP_TX_BUF(ep) - EPM) * WSIZE; - desc[0] = addr_tx | (count << 16); + uint32_t addr_tx = (EPM_EP_BUF(ep, bufnum) - EPM) * WSIZE; + *desc = addr_tx | (count << 16); } } @@ -108,18 +106,22 @@ epm_set_ep_count_tx(int ep, uint32_t count) static void btable_configure(void) { - epm_ep_desc_setup(0, USB_CDC_EP0_SIZE); - epm_ep_desc_setup(USB_CDC_EP_ACM, 0); - epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, USB_CDC_EP_BULK_OUT_SIZE); - epm_ep_desc_setup(USB_CDC_EP_BULK_IN, 0); + epm_ep_desc_setup(0, BUFTX, 0); + epm_ep_desc_setup(0, BUFRX, USB_CDC_EP0_SIZE); + epm_ep_desc_setup(USB_CDC_EP_ACM, BUFTX, 0); + epm_ep_desc_setup(USB_CDC_EP_ACM, BUFRX, 0); + epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, BUFTX, 0); + epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, BUFRX, USB_CDC_EP_BULK_OUT_SIZE); + epm_ep_desc_setup(USB_CDC_EP_BULK_IN, BUFTX, 0); + epm_ep_desc_setup(USB_CDC_EP_BULK_IN, BUFRX, 0); } // Read a packet stored in dedicated usb memory static uint32_t -btable_read_packet(int ep, uint8_t *dest, int max_len) +btable_read_packet(int ep, int bufnum, uint8_t *dest, int max_len) { - epmword_t *src = EPM_EP_RX_BUF(ep); - uint32_t count = epm_get_ep_count_rx(ep); + epmword_t *src = EPM_EP_BUF(ep, bufnum); + uint32_t count = epm_get_ep_count_rx(ep, bufnum); if (count > max_len) count = max_len; int i; @@ -145,9 +147,9 @@ btable_read_packet(int ep, uint8_t *dest, int max_len) // Write a packet to dedicated usb memory static void -btable_write_packet(int ep, const uint8_t *src, int count) +btable_write_packet(int ep, int bufnum, const uint8_t *src, int count) { - epmword_t *dest = EPM_EP_TX_BUF(ep); + epmword_t *dest = EPM_EP_BUF(ep, bufnum); int i; for (i=0; i Date: Sat, 23 Sep 2023 11:45:39 -0400 Subject: [PATCH 07/21] stm32: Add usbfs double buffer support for bulk rx messages Implement the usbfs fast buffer switching mechanism on the "bulk out" endpoint. This can improve the overall USB throughput and bus utilization. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 63 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 3c96fd4ddbce..661aa1b1585d 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -48,6 +48,12 @@ #define USB_CNTR_FRES USB_CNTR_USBRST #endif +// Some chip variants do not define these fields +#ifndef USB_EP_DTOG_TX_Pos +#define USB_EP_DTOG_TX_Pos 6 +#define USB_EP_DTOG_RX_Pos 14 +#endif + /**************************************************************** * USB transfer memory @@ -110,8 +116,8 @@ btable_configure(void) epm_ep_desc_setup(0, BUFRX, USB_CDC_EP0_SIZE); epm_ep_desc_setup(USB_CDC_EP_ACM, BUFTX, 0); epm_ep_desc_setup(USB_CDC_EP_ACM, BUFRX, 0); - epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, BUFTX, 0); - epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, BUFRX, USB_CDC_EP_BULK_OUT_SIZE); + epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, 0, USB_CDC_EP_BULK_OUT_SIZE); + epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, 1, USB_CDC_EP_BULK_OUT_SIZE); epm_ep_desc_setup(USB_CDC_EP_BULK_IN, BUFTX, 0); epm_ep_desc_setup(USB_CDC_EP_BULK_IN, BUFRX, 0); } @@ -192,20 +198,41 @@ calc_epr_bits(uint32_t epr, uint32_t mask, uint32_t value) return (((epr & (EPR_RWBITS | tmask)) ^ tvalue) & ~rwmask) | rwbits | cbits; } +// Check if double buffering endpoint hardware can no longer send/receive +static int +epr_is_dbuf_blocking(uint32_t epr) +{ + return !(((epr >> (USB_EP_DTOG_RX_Pos - USB_EP_DTOG_TX_Pos)) ^ epr) + & USB_EP_DTOG_TX); +} + /**************************************************************** * USB interface ****************************************************************/ +static uint32_t bulk_out_pop_count, bulk_out_push_flag; + int_fast8_t usb_read_bulk_out(void *data, uint_fast8_t max_len) { - uint32_t ep = USB_CDC_EP_BULK_OUT, epr = USB_EPR[ep]; - if ((epr & USB_EPRX_STAT) == USB_EP_RX_VALID) + if (readl(&bulk_out_push_flag)) // No data ready return -1; - uint32_t count = btable_read_packet(ep, BUFRX, data, max_len); - USB_EPR[ep] = calc_epr_bits(epr, USB_EPRX_STAT, USB_EP_RX_VALID); + uint32_t ep = USB_CDC_EP_BULK_OUT; + int bufnum = bulk_out_pop_count & 1; + bulk_out_pop_count++; + uint32_t count = btable_read_packet(ep, bufnum, data, max_len); + writel(&bulk_out_push_flag, USB_EP_DTOG_TX); + + // Check if irq handler pulled another packet before push flag update + uint32_t epr = USB_EPR[ep]; + if (epr_is_dbuf_blocking(epr) && readl(&bulk_out_push_flag)) { + // Second packet was already read - must notify hardware + writel(&bulk_out_push_flag, 0); + USB_EPR[ep] = calc_epr_bits(epr, 0, 0) | USB_EP_DTOG_TX; + } + return count; } @@ -275,6 +302,9 @@ usb_set_address(uint_fast8_t addr) void usb_set_configure(void) { + uint32_t ep = USB_CDC_EP_BULK_OUT; + bulk_out_pop_count = 0; + USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPRX_STAT, USB_EP_RX_VALID); } @@ -294,8 +324,9 @@ usb_reset(void) | USB_EP_RX_NAK | USB_EP_TX_NAK); ep = USB_CDC_EP_BULK_OUT; - USB_EPR[ep] = (USB_CDC_EP_BULK_OUT | USB_EP_BULK - | USB_EP_RX_VALID | USB_EP_TX_NAK); + USB_EPR[ep] = (USB_CDC_EP_BULK_OUT | USB_EP_BULK | USB_EP_KIND + | USB_EP_RX_VALID | USB_EP_TX_NAK | USB_EP_DTOG_TX); + bulk_out_push_flag = USB_EP_DTOG_TX; ep = USB_CDC_EP_BULK_IN; USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK @@ -313,18 +344,22 @@ USB_IRQHandler(void) if (istr & USB_ISTR_CTR) { // Endpoint activity uint32_t ep = istr & USB_ISTR_EP_ID, epr = USB_EPR[ep]; - USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); - if (ep == 0) { + if (ep == USB_CDC_EP_BULK_OUT) { + USB_EPR[ep] = (calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0) + | bulk_out_push_flag); + bulk_out_push_flag = 0; + usb_notify_bulk_out(); + } else if (ep == USB_CDC_EP_BULK_IN) { + USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); + usb_notify_bulk_in(); + } else if (ep == 0) { + USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); usb_notify_ep0(); if (epr & USB_EP_CTR_TX && set_address) { // Apply address after last "in" message transmitted USB->DADDR = set_address; set_address = 0; } - } else if (ep == USB_CDC_EP_BULK_OUT) { - usb_notify_bulk_out(); - } else if (ep == USB_CDC_EP_BULK_IN) { - usb_notify_bulk_in(); } } if (istr & USB_ISTR_RESET) { From cd8d57c2c68049e8fbbeedd88eba01fe6495c7c5 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sat, 23 Sep 2023 15:31:52 -0400 Subject: [PATCH 08/21] stm32: Add usbfs double buffer support for bulk tx messages Implement the usbfs fast buffer switching mechanism on the "bulk in" endpoint. This can improve the overall USB throughput and bus utilization. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 661aa1b1585d..d166fbb7ace9 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -118,8 +118,8 @@ btable_configure(void) epm_ep_desc_setup(USB_CDC_EP_ACM, BUFRX, 0); epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, 0, USB_CDC_EP_BULK_OUT_SIZE); epm_ep_desc_setup(USB_CDC_EP_BULK_OUT, 1, USB_CDC_EP_BULK_OUT_SIZE); - epm_ep_desc_setup(USB_CDC_EP_BULK_IN, BUFTX, 0); - epm_ep_desc_setup(USB_CDC_EP_BULK_IN, BUFRX, 0); + epm_ep_desc_setup(USB_CDC_EP_BULK_IN, 0, 0); + epm_ep_desc_setup(USB_CDC_EP_BULK_IN, 1, 0); } // Read a packet stored in dedicated usb memory @@ -236,15 +236,27 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) return count; } +static uint32_t bulk_in_push_count, bulk_in_pop_flag; + int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) { - uint32_t ep = USB_CDC_EP_BULK_IN, epr = USB_EPR[ep]; - if ((epr & USB_EPTX_STAT) != USB_EP_TX_NAK) + if (readl(&bulk_in_pop_flag)) // No buffer space available return -1; - btable_write_packet(ep, BUFTX, data, len); - USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT, USB_EP_TX_VALID); + uint32_t ep = USB_CDC_EP_BULK_IN; + int bufnum = bulk_in_push_count & 1; + bulk_in_push_count++; + btable_write_packet(ep, bufnum, data, len); + writel(&bulk_in_pop_flag, USB_EP_DTOG_RX); + + // Check if hardware needs to be notified + uint32_t epr = USB_EPR[ep]; + if (epr_is_dbuf_blocking(epr) && readl(&bulk_in_pop_flag)) { + writel(&bulk_in_pop_flag, 0); + USB_EPR[ep] = calc_epr_bits(epr, 0, 0) | USB_EP_DTOG_RX; + } + return len; } @@ -305,6 +317,11 @@ usb_set_configure(void) uint32_t ep = USB_CDC_EP_BULK_OUT; bulk_out_pop_count = 0; USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPRX_STAT, USB_EP_RX_VALID); + + ep = USB_CDC_EP_BULK_IN; + bulk_in_push_count = 0; + writel(&bulk_in_pop_flag, 0); + USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPTX_STAT, USB_EP_TX_VALID); } @@ -329,8 +346,9 @@ usb_reset(void) bulk_out_push_flag = USB_EP_DTOG_TX; ep = USB_CDC_EP_BULK_IN; - USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK + USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK | USB_EP_KIND | USB_EP_RX_NAK | USB_EP_TX_NAK); + bulk_in_pop_flag = USB_EP_DTOG_RX; USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM; USB->DADDR = USB_DADDR_EF; @@ -350,7 +368,9 @@ USB_IRQHandler(void) bulk_out_push_flag = 0; usb_notify_bulk_out(); } else if (ep == USB_CDC_EP_BULK_IN) { - USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); + USB_EPR[ep] = (calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0) + | bulk_in_pop_flag); + bulk_in_pop_flag = 0; usb_notify_bulk_in(); } else if (ep == 0) { USB_EPR[ep] = calc_epr_bits(epr, USB_EP_CTR_RX | USB_EP_CTR_TX, 0); From 83eecae0281634279a7b891561af353105f2ad2c Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 24 Sep 2023 01:22:31 -0400 Subject: [PATCH 09/21] rp2040: Add helper functions to usbserial.c Add helper functions for manipulating the buffer memory and packet control registers. This is in preparation for double buffer support. Signed-off-by: Kevin O'Connor --- src/rp2040/usbserial.c | 76 +++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 20 deletions(-) diff --git a/src/rp2040/usbserial.c b/src/rp2040/usbserial.c index e63e590dc640..f2db3b5e7379 100644 --- a/src/rp2040/usbserial.c +++ b/src/rp2040/usbserial.c @@ -26,29 +26,67 @@ #define DPBUF_SIZE 64 +// Get the offset of a given endpoint's base buffer static uint32_t usb_buf_offset(uint32_t ep) { return 0x100 + ep * DPBUF_SIZE * 2; } +// Obtain a pointer to an endpoint buffer +static void* +usb_buf_addr(uint32_t ep, int bufnum) +{ + return (void*)usb_dpram + usb_buf_offset(ep) + bufnum * DPBUF_SIZE; +} + +// Return a pointer to the ep_buf_ctrl register for an endpoint +static volatile uint16_t * +lookup_epbufctrl(uint32_t ep, int is_rx, int bufnum) +{ + volatile uint16_t *epbp; + if (is_rx) + epbp = (void*)&usb_dpram->ep_buf_ctrl[ep].out; + else + epbp = (void*)&usb_dpram->ep_buf_ctrl[ep].in; + return &epbp[bufnum]; +} + +// Determine the next transfer PID id from the last PID +static uint32_t +next_data_pid(uint32_t epb) +{ + return (epb ^ USB_BUF_CTRL_DATA1_PID) & USB_BUF_CTRL_DATA1_PID; +} + +// Extract the number of bytes in an rx buffer +static uint32_t +get_rx_count(uint32_t epb, uint32_t max_len) +{ + uint32_t c = epb & USB_BUF_CTRL_LEN_MASK; + if (c > max_len) + c = max_len; + return c; +} + static int_fast8_t usb_write_packet(uint32_t ep, const void *data, uint_fast8_t len) { // Check if there is room for this packet - uint32_t epb = usb_dpram->ep_buf_ctrl[ep].in; + volatile uint16_t *epbp = lookup_epbufctrl(ep, 0, 0); + uint32_t epb = *epbp; if (epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) return -1; - uint32_t pid = (epb ^ USB_BUF_CTRL_DATA1_PID) & USB_BUF_CTRL_DATA1_PID; + // Determine the next packet header + uint32_t pid = next_data_pid(epb); uint32_t new_epb = USB_BUF_CTRL_FULL | USB_BUF_CTRL_LAST | pid | len; - usb_dpram->ep_buf_ctrl[ep].in = new_epb; - // Copy the packet to the hw buffer - void *addr = (void*)usb_dpram + usb_buf_offset(ep); - barrier(); - memcpy(addr, data, len); + *epbp = new_epb; barrier(); + // Copy the packet to the hw buffer + memcpy(usb_buf_addr(ep, 0), data, len); // Inform the USB hardware of the available packet - usb_dpram->ep_buf_ctrl[ep].in = new_epb | USB_BUF_CTRL_AVAIL; + barrier(); + *epbp = new_epb | USB_BUF_CTRL_AVAIL; return len; } @@ -56,22 +94,20 @@ static int_fast8_t usb_read_packet(uint32_t ep, void *data, uint_fast8_t max_len) { // Check if there is a packet ready - uint32_t epb = usb_dpram->ep_buf_ctrl[ep].out; + volatile uint16_t *epbp = lookup_epbufctrl(ep, 1, 0); + uint32_t epb = *epbp; if ((epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) != USB_BUF_CTRL_FULL) return -1; - // Copy the packet to the given buffer - uint32_t pid = (epb ^ USB_BUF_CTRL_DATA1_PID) & USB_BUF_CTRL_DATA1_PID; - uint32_t new_epb = USB_BUF_CTRL_LAST | pid | DPBUF_SIZE; - usb_dpram->ep_buf_ctrl[ep].out = new_epb; - uint32_t c = epb & USB_BUF_CTRL_LEN_MASK; - if (c > max_len) - c = max_len; - void *addr = (void*)usb_dpram + usb_buf_offset(ep); - barrier(); - memcpy(data, addr, c); + // Determine the next packet header + uint32_t new_epb = USB_BUF_CTRL_LAST | next_data_pid(epb) | DPBUF_SIZE; + *epbp = new_epb; barrier(); + // Copy the packet to the given buffer + uint32_t c = get_rx_count(epb, max_len); + memcpy(data, usb_buf_addr(ep, 0), c); // Notify the USB hardware that the space is now available - usb_dpram->ep_buf_ctrl[ep].out = new_epb | USB_BUF_CTRL_AVAIL; + barrier(); + *epbp = new_epb | USB_BUF_CTRL_AVAIL; return c; } From bdeec0f56da9b4f8a9e2f9e34a7e7f599a2ba34d Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 24 Sep 2023 23:39:44 -0400 Subject: [PATCH 10/21] rp2040: Open code usb_read_packet() and usb_write_packet() in callers Copy the code for these two functions to their respective callers. This is in preparation for double buffer support. Signed-off-by: Kevin O'Connor --- src/rp2040/usbserial.c | 94 ++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/src/rp2040/usbserial.c b/src/rp2040/usbserial.c index f2db3b5e7379..ad08cb46d639 100644 --- a/src/rp2040/usbserial.c +++ b/src/rp2040/usbserial.c @@ -69,31 +69,16 @@ get_rx_count(uint32_t epb, uint32_t max_len) return c; } -static int_fast8_t -usb_write_packet(uint32_t ep, const void *data, uint_fast8_t len) -{ - // Check if there is room for this packet - volatile uint16_t *epbp = lookup_epbufctrl(ep, 0, 0); - uint32_t epb = *epbp; - if (epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) - return -1; - // Determine the next packet header - uint32_t pid = next_data_pid(epb); - uint32_t new_epb = USB_BUF_CTRL_FULL | USB_BUF_CTRL_LAST | pid | len; - *epbp = new_epb; - barrier(); - // Copy the packet to the hw buffer - memcpy(usb_buf_addr(ep, 0), data, len); - // Inform the USB hardware of the available packet - barrier(); - *epbp = new_epb | USB_BUF_CTRL_AVAIL; - return len; -} -static int_fast8_t -usb_read_packet(uint32_t ep, void *data, uint_fast8_t max_len) +/**************************************************************** + * Interface + ****************************************************************/ + +int_fast8_t +usb_read_bulk_out(void *data, uint_fast8_t max_len) { // Check if there is a packet ready + uint32_t ep = USB_CDC_EP_BULK_OUT; volatile uint16_t *epbp = lookup_epbufctrl(ep, 1, 0); uint32_t epb = *epbp; if ((epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) != USB_BUF_CTRL_FULL) @@ -111,21 +96,26 @@ usb_read_packet(uint32_t ep, void *data, uint_fast8_t max_len) return c; } - -/**************************************************************** - * Interface - ****************************************************************/ - -int_fast8_t -usb_read_bulk_out(void *data, uint_fast8_t max_len) -{ - return usb_read_packet(USB_CDC_EP_BULK_OUT, data, max_len); -} - int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) { - return usb_write_packet(USB_CDC_EP_BULK_IN, data, len); + // Check if there is room for this packet + uint32_t ep = USB_CDC_EP_BULK_IN; + volatile uint16_t *epbp = lookup_epbufctrl(ep, 0, 0); + uint32_t epb = *epbp; + if (epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) + return -1; + // Determine the next packet header + uint32_t pid = next_data_pid(epb); + uint32_t new_epb = USB_BUF_CTRL_FULL | USB_BUF_CTRL_LAST | pid | len; + *epbp = new_epb; + barrier(); + // Copy the packet to the hw buffer + memcpy(usb_buf_addr(ep, 0), data, len); + // Inform the USB hardware of the available packet + barrier(); + *epbp = new_epb | USB_BUF_CTRL_AVAIL; + return len; } int_fast8_t @@ -154,19 +144,51 @@ usb_read_ep0_setup(void *data, uint_fast8_t max_len) int_fast8_t usb_read_ep0(void *data, uint_fast8_t max_len) { + // Check if there is a packet ready + uint32_t ep = 0; if (usb_hw->intr & USB_INTR_SETUP_REQ_BITS) // Early end of transmission return -2; - return usb_read_packet(0, data, max_len); + volatile uint16_t *epbp = lookup_epbufctrl(ep, 1, 0); + uint32_t epb = *epbp; + if ((epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) != USB_BUF_CTRL_FULL) + return -1; + // Determine the next packet header + uint32_t new_epb = USB_BUF_CTRL_LAST | next_data_pid(epb) | DPBUF_SIZE; + *epbp = new_epb; + barrier(); + // Copy the packet to the given buffer + uint32_t c = get_rx_count(epb, max_len); + memcpy(data, usb_buf_addr(ep, 0), c); + // Notify the USB hardware that the space is now available + barrier(); + *epbp = new_epb | USB_BUF_CTRL_AVAIL; + return c; } int_fast8_t usb_send_ep0(const void *data, uint_fast8_t len) { + // Check if there is room for this packet + uint32_t ep = 0; if (usb_hw->intr & USB_INTR_SETUP_REQ_BITS || usb_hw->buf_status & 2) // Early end of transmission return -2; - return usb_write_packet(0, data, len); + volatile uint16_t *epbp = lookup_epbufctrl(ep, 0, 0); + uint32_t epb = *epbp; + if (epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) + return -1; + // Determine the next packet header + uint32_t pid = next_data_pid(epb); + uint32_t new_epb = USB_BUF_CTRL_FULL | USB_BUF_CTRL_LAST | pid | len; + *epbp = new_epb; + barrier(); + // Copy the packet to the hw buffer + memcpy(usb_buf_addr(ep, 0), data, len); + // Inform the USB hardware of the available packet + barrier(); + *epbp = new_epb | USB_BUF_CTRL_AVAIL; + return len; } void From 90427fe30ee518f22f2ebba716daf8cedf4020e2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 24 Sep 2023 02:10:57 -0400 Subject: [PATCH 11/21] rp2040: Add support for double buffering on USB bulk rx packets Signed-off-by: Kevin O'Connor --- src/rp2040/usbserial.c | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/rp2040/usbserial.c b/src/rp2040/usbserial.c index ad08cb46d639..673adc6bd0a2 100644 --- a/src/rp2040/usbserial.c +++ b/src/rp2040/usbserial.c @@ -1,6 +1,6 @@ // Hardware interface to USB on rp2040 // -// Copyright (C) 2021 Kevin O'Connor +// Copyright (C) 2021-2023 Kevin O'Connor // // This file may be distributed under the terms of the GNU GPLv3 license. @@ -74,22 +74,27 @@ get_rx_count(uint32_t epb, uint32_t max_len) * Interface ****************************************************************/ +static uint32_t bulk_out_push_count; + int_fast8_t usb_read_bulk_out(void *data, uint_fast8_t max_len) { // Check if there is a packet ready + uint32_t bopc = bulk_out_push_count, bufnum = bopc & 1; uint32_t ep = USB_CDC_EP_BULK_OUT; - volatile uint16_t *epbp = lookup_epbufctrl(ep, 1, 0); + volatile uint16_t *epbp = lookup_epbufctrl(ep, 1, bufnum); uint32_t epb = *epbp; if ((epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) != USB_BUF_CTRL_FULL) return -1; // Determine the next packet header - uint32_t new_epb = USB_BUF_CTRL_LAST | next_data_pid(epb) | DPBUF_SIZE; + bulk_out_push_count = bopc + 1; + uint32_t pid = bufnum ? USB_BUF_CTRL_DATA1_PID : 0; + uint32_t new_epb = USB_BUF_CTRL_LAST | pid | DPBUF_SIZE; *epbp = new_epb; barrier(); // Copy the packet to the given buffer uint32_t c = get_rx_count(epb, max_len); - memcpy(data, usb_buf_addr(ep, 0), c); + memcpy(data, usb_buf_addr(ep, bufnum), c); // Notify the USB hardware that the space is now available barrier(); *epbp = new_epb | USB_BUF_CTRL_AVAIL; @@ -215,8 +220,11 @@ void usb_set_configure(void) { usb_dpram->ep_buf_ctrl[USB_CDC_EP_BULK_IN].in = USB_BUF_CTRL_DATA1_PID; - usb_dpram->ep_buf_ctrl[USB_CDC_EP_BULK_OUT].out = ( - USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LAST | DPBUF_SIZE); + + bulk_out_push_count = 0; + uint32_t epb0 = USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LAST | DPBUF_SIZE; + uint32_t epb1 = epb0 | USB_BUF_CTRL_DATA1_PID; + usb_dpram->ep_buf_ctrl[USB_CDC_EP_BULK_OUT].out = epb0 | (epb1 << 16); } @@ -348,6 +356,7 @@ endpoint_setup(void) usb_dpram->ep_ctrl[USB_CDC_EP_ACM-1].in = ep_acm; // BULK uint32_t ep_out = (EP_CTRL_ENABLE_BITS | usb_buf_offset(USB_CDC_EP_BULK_OUT) + | EP_CTRL_DOUBLE_BUFFERED_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | (USB_ENDPOINT_XFER_BULK << EP_CTRL_BUFFER_TYPE_LSB)); usb_dpram->ep_ctrl[USB_CDC_EP_BULK_OUT-1].out = ep_out; From 472fd32cabadf6590a32f41c89d55095cc7ab59f Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Sun, 24 Sep 2023 02:24:54 -0400 Subject: [PATCH 12/21] rp2040: Add support for double buffering on USB bulk tx packets Signed-off-by: Kevin O'Connor --- src/rp2040/usbserial.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/rp2040/usbserial.c b/src/rp2040/usbserial.c index 673adc6bd0a2..61d04d1891a5 100644 --- a/src/rp2040/usbserial.c +++ b/src/rp2040/usbserial.c @@ -101,22 +101,26 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) return c; } +static uint32_t bulk_in_pop_count; + int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) { // Check if there is room for this packet + uint32_t bipc = bulk_in_pop_count, bufnum = bipc & 1; uint32_t ep = USB_CDC_EP_BULK_IN; - volatile uint16_t *epbp = lookup_epbufctrl(ep, 0, 0); + volatile uint16_t *epbp = lookup_epbufctrl(ep, 0, bufnum); uint32_t epb = *epbp; if (epb & (USB_BUF_CTRL_AVAIL|USB_BUF_CTRL_FULL)) return -1; // Determine the next packet header - uint32_t pid = next_data_pid(epb); + bulk_in_pop_count = bipc + 1; + uint32_t pid = bufnum ? USB_BUF_CTRL_DATA1_PID : 0; uint32_t new_epb = USB_BUF_CTRL_FULL | USB_BUF_CTRL_LAST | pid | len; *epbp = new_epb; barrier(); // Copy the packet to the hw buffer - memcpy(usb_buf_addr(ep, 0), data, len); + memcpy(usb_buf_addr(ep, bufnum), data, len); // Inform the USB hardware of the available packet barrier(); *epbp = new_epb | USB_BUF_CTRL_AVAIL; @@ -219,7 +223,8 @@ usb_set_address(uint_fast8_t addr) void usb_set_configure(void) { - usb_dpram->ep_buf_ctrl[USB_CDC_EP_BULK_IN].in = USB_BUF_CTRL_DATA1_PID; + bulk_in_pop_count = 0; + usb_dpram->ep_buf_ctrl[USB_CDC_EP_BULK_IN].in = 0; bulk_out_push_count = 0; uint32_t epb0 = USB_BUF_CTRL_AVAIL | USB_BUF_CTRL_LAST | DPBUF_SIZE; @@ -361,6 +366,7 @@ endpoint_setup(void) | (USB_ENDPOINT_XFER_BULK << EP_CTRL_BUFFER_TYPE_LSB)); usb_dpram->ep_ctrl[USB_CDC_EP_BULK_OUT-1].out = ep_out; uint32_t ep_in = (EP_CTRL_ENABLE_BITS | usb_buf_offset(USB_CDC_EP_BULK_IN) + | EP_CTRL_DOUBLE_BUFFERED_BITS | EP_CTRL_INTERRUPT_PER_BUFFER | (USB_ENDPOINT_XFER_BULK << EP_CTRL_BUFFER_TYPE_LSB)); usb_dpram->ep_ctrl[USB_CDC_EP_BULK_IN-1].in = ep_in; From 5b204866c52bea7629f1e95ab345d0bbe8637764 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 25 Sep 2023 11:09:40 -0400 Subject: [PATCH 13/21] usb_canbus: Rename UsbCan.queue to UsbCan.canhw_queue Rename the internal variable names. This is in preparation for support of a USB message queue. Signed-off-by: Kevin O'Connor --- src/generic/usb_canbus.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/generic/usb_canbus.c b/src/generic/usb_canbus.c index d776d45f248a..0cafac48fa72 100644 --- a/src/generic/usb_canbus.c +++ b/src/generic/usb_canbus.c @@ -116,8 +116,8 @@ static struct usbcan_data { uint32_t assigned_id; // Data from physical canbus interface - uint32_t pull_pos, push_pos; - struct canbus_msg queue[32]; + uint32_t canhw_pull_pos, canhw_push_pos; + struct canbus_msg canhw_queue[32]; } UsbCan; enum { @@ -139,16 +139,16 @@ void canbus_process_data(struct canbus_msg *msg) { // Add to admin command queue - uint32_t pushp = UsbCan.push_pos; - if (pushp - UsbCan.pull_pos >= ARRAY_SIZE(UsbCan.queue)) + uint32_t pushp = UsbCan.canhw_push_pos; + if (pushp - UsbCan.canhw_pull_pos >= ARRAY_SIZE(UsbCan.canhw_queue)) // No space - drop message return; if (UsbCan.assigned_id && (msg->id & ~1) == UsbCan.assigned_id) // Id reserved for local return; - uint32_t pos = pushp % ARRAY_SIZE(UsbCan.queue); - memcpy(&UsbCan.queue[pos], msg, sizeof(*msg)); - UsbCan.push_pos = pushp + 1; + uint32_t pos = pushp % ARRAY_SIZE(UsbCan.canhw_queue); + memcpy(&UsbCan.canhw_queue[pos], msg, sizeof(*msg)); + UsbCan.canhw_push_pos = pushp + 1; usb_notify_bulk_out(); } @@ -167,24 +167,24 @@ send_frame(struct canbus_msg *msg) // Send any pending hw frames to host static void -drain_hw_queue(void) +drain_canhw_queue(void) { - uint32_t pull_pos = UsbCan.pull_pos; + uint32_t pull_pos = UsbCan.canhw_pull_pos; for (;;) { - uint32_t push_pos = readl(&UsbCan.push_pos); + uint32_t push_pos = readl(&UsbCan.canhw_push_pos); if (push_pos == pull_pos) { // No more data to send UsbCan.usb_send_busy = 0; return; } - uint32_t pos = pull_pos % ARRAY_SIZE(UsbCan.queue); - int ret = send_frame(&UsbCan.queue[pos]); + uint32_t pos = pull_pos % ARRAY_SIZE(UsbCan.canhw_queue); + int ret = send_frame(&UsbCan.canhw_queue[pos]); if (ret < 0) { // USB is busy - retry later UsbCan.usb_send_busy = 1; return; } - UsbCan.pull_pos = pull_pos = pull_pos + 1; + UsbCan.canhw_pull_pos = pull_pos = pull_pos + 1; } } @@ -195,7 +195,7 @@ usbcan_task(void) return; // Send any pending hw frames to host - drain_hw_queue(); + drain_canhw_queue(); for (;;) { // See if previous host frame needs to be transmitted From 78ae83c3141e5e46ca109e92adc015e3698f078b Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 25 Sep 2023 11:34:31 -0400 Subject: [PATCH 14/21] usb_canbus: Add a local queue for USB messages received from host Read USB messages arriving from the host into a queue. This makes it less likely that USB "bulk out" packets will be NAK'ed on the USB bus, which improves USB bus utilization. Signed-off-by: Kevin O'Connor --- src/generic/usb_canbus.c | 53 ++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/src/generic/usb_canbus.c b/src/generic/usb_canbus.c index 0cafac48fa72..9d09adf9e783 100644 --- a/src/generic/usb_canbus.c +++ b/src/generic/usb_canbus.c @@ -104,17 +104,15 @@ struct gs_host_frame { static struct usbcan_data { struct task_wake wake; - // Canbus data from host - union { - struct gs_host_frame host_frame; - uint8_t rx_frame_pad[USB_CDC_EP_BULK_OUT_SIZE]; - }; - uint8_t host_status; - // Canbus data routed locally uint8_t notify_local, usb_send_busy; uint32_t assigned_id; + // Canbus data from host + uint8_t host_status; + uint32_t host_pull_pos, host_push_pos; + struct gs_host_frame host_frames[16]; + // Data from physical canbus interface uint32_t canhw_pull_pos, canhw_push_pos; struct canbus_msg canhw_queue[32]; @@ -188,6 +186,25 @@ drain_canhw_queue(void) } } +// Fill local queue with any USB messages sent from host +static void +fill_usb_host_queue(void) +{ + uint32_t pull_pos = UsbCan.host_pull_pos, push_pos = UsbCan.host_push_pos; + for (;;) { + if (push_pos - pull_pos >= ARRAY_SIZE(UsbCan.host_frames)) + // No more space in queue + break; + uint32_t pushp = push_pos % ARRAY_SIZE(UsbCan.host_frames); + struct gs_host_frame *gs = &UsbCan.host_frames[pushp]; + int ret = usb_read_bulk_out(gs, sizeof(*gs)); + if (ret <= 0) + // No more messages ready + break; + UsbCan.host_push_pos = push_pos = push_pos + 1; + } +} + void usbcan_task(void) { @@ -197,11 +214,17 @@ usbcan_task(void) // Send any pending hw frames to host drain_canhw_queue(); + // Fill local queue with any USB messages arriving from host + fill_usb_host_queue(); + + // Route messages received from host + uint32_t pull_pos = UsbCan.host_pull_pos, push_pos = UsbCan.host_push_pos; + uint32_t pullp = pull_pos % ARRAY_SIZE(UsbCan.host_frames); + struct gs_host_frame *gs = &UsbCan.host_frames[pullp]; for (;;) { // See if previous host frame needs to be transmitted uint_fast8_t host_status = UsbCan.host_status; if (host_status & (HS_TX_HW | HS_TX_LOCAL)) { - struct gs_host_frame *gs = &UsbCan.host_frame; struct canbus_msg msg; msg.id = gs->can_id; msg.dlc = gs->can_dlc; @@ -224,20 +247,19 @@ usbcan_task(void) if (UsbCan.usb_send_busy) // Don't send echo frame until other traffic is sent return; - int ret = usb_send_bulk_in(&UsbCan.host_frame - , sizeof(UsbCan.host_frame)); + int ret = usb_send_bulk_in(gs, sizeof(*gs)); if (ret < 0) return; UsbCan.host_status = 0; + UsbCan.host_pull_pos = pull_pos = pull_pos + 1; } - // Read next frame from host - int ret = usb_read_bulk_out(&UsbCan.host_frame - , USB_CDC_EP_BULK_OUT_SIZE); - if (ret <= 0) + // Process next frame from host + if (pull_pos == push_pos) // No frame available - no more work to be done break; - uint32_t id = UsbCan.host_frame.can_id; + gs = &UsbCan.host_frames[pull_pos % ARRAY_SIZE(UsbCan.host_frames)]; + uint32_t id = gs->can_id; UsbCan.host_status = HS_TX_ECHO | HS_TX_HW; if (id == CANBUS_ID_ADMIN) UsbCan.host_status = HS_TX_ECHO | HS_TX_HW | HS_TX_LOCAL; @@ -245,6 +267,7 @@ usbcan_task(void) UsbCan.host_status = HS_TX_ECHO | HS_TX_LOCAL; } + // Wake up local message response handling (if usb is not busy) if (UsbCan.notify_local && !UsbCan.usb_send_busy) canserial_notify_tx(); } From 6adff3954b149877c8a587893b18b291e34856b2 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Tue, 26 Sep 2023 15:24:29 -0400 Subject: [PATCH 15/21] usb_canbus: Prioritize local response sending over new host messages Prioritize sending responses back to the host over transmitting new messages from the host. Otherwise, the gs_usb host usb acknowledgments could saturate the usb bandwidth for extended periods. Signed-off-by: Kevin O'Connor --- src/generic/usb_canbus.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/generic/usb_canbus.c b/src/generic/usb_canbus.c index 9d09adf9e783..9c2893bf231b 100644 --- a/src/generic/usb_canbus.c +++ b/src/generic/usb_canbus.c @@ -244,9 +244,9 @@ usbcan_task(void) // Send any previous echo frames if (host_status) { - if (UsbCan.usb_send_busy) + if (UsbCan.notify_local || UsbCan.usb_send_busy) // Don't send echo frame until other traffic is sent - return; + break; int ret = usb_send_bulk_in(gs, sizeof(*gs)); if (ret < 0) return; @@ -281,6 +281,8 @@ canbus_send(struct canbus_msg *msg) int ret = send_frame(msg); if (ret < 0) goto retry_later; + if (UsbCan.notify_local && UsbCan.host_status) + canbus_notify_tx(); UsbCan.notify_local = 0; return msg->dlc; retry_later: From 615db729e7f35949860f0c9791d6124e1a8fb725 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 4 Oct 2023 18:49:46 -0400 Subject: [PATCH 16/21] stm32: Only enable one direction on usbfs double buffered end points The bulk out endpoint should not be enabled in tx mode, and the bulk in endpoint should not be enabled in rx mode. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index d166fbb7ace9..4349b6af1c58 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -342,12 +342,12 @@ usb_reset(void) ep = USB_CDC_EP_BULK_OUT; USB_EPR[ep] = (USB_CDC_EP_BULK_OUT | USB_EP_BULK | USB_EP_KIND - | USB_EP_RX_VALID | USB_EP_TX_NAK | USB_EP_DTOG_TX); + | USB_EP_RX_NAK | USB_EP_DTOG_TX); bulk_out_push_flag = USB_EP_DTOG_TX; ep = USB_CDC_EP_BULK_IN; USB_EPR[ep] = (USB_CDC_EP_BULK_IN | USB_EP_BULK | USB_EP_KIND - | USB_EP_RX_NAK | USB_EP_TX_NAK); + | USB_EP_TX_NAK); bulk_in_pop_flag = USB_EP_DTOG_RX; USB->CNTR = USB_CNTR_CTRM | USB_CNTR_RESETM; From 043f18da260378d11e550248c032702017dafb49 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 4 Oct 2023 19:34:22 -0400 Subject: [PATCH 17/21] stm32: Fix usbfs spurious USB packet transmit on startup Commit cd8d57c2 added USB double buffering mode on transmits. However, when enabling double buffering mode, the hardware seems to always send at least two packets. Spurious transmissions could cause the Linux gs_usb driver to get confused, which could lead to the can0 device becoming unavailable on restarts. Fix by waiting for two USB packets to be available before enabling the endpoint. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 4349b6af1c58..0ed2e0ee5e5d 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -236,7 +236,8 @@ usb_read_bulk_out(void *data, uint_fast8_t max_len) return count; } -static uint32_t bulk_in_push_count, bulk_in_pop_flag; +static uint32_t bulk_in_push_pos, bulk_in_pop_flag; +#define BI_START 2 int_fast8_t usb_send_bulk_in(void *data, uint_fast8_t len) @@ -245,8 +246,8 @@ usb_send_bulk_in(void *data, uint_fast8_t len) // No buffer space available return -1; uint32_t ep = USB_CDC_EP_BULK_IN; - int bufnum = bulk_in_push_count & 1; - bulk_in_push_count++; + uint32_t bipp = bulk_in_push_pos, bufnum = bipp & 1; + bulk_in_push_pos = bipp ^ 1; btable_write_packet(ep, bufnum, data, len); writel(&bulk_in_pop_flag, USB_EP_DTOG_RX); @@ -254,7 +255,17 @@ usb_send_bulk_in(void *data, uint_fast8_t len) uint32_t epr = USB_EPR[ep]; if (epr_is_dbuf_blocking(epr) && readl(&bulk_in_pop_flag)) { writel(&bulk_in_pop_flag, 0); - USB_EPR[ep] = calc_epr_bits(epr, 0, 0) | USB_EP_DTOG_RX; + if (bipp & BI_START) { + // Two packets are always sent when starting in double + // buffering mode, so wait for second packet before starting. + if (bipp == (BI_START | 1)) { + bulk_in_push_pos = 0; + USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT + , USB_EP_TX_VALID); + } + } else { + USB_EPR[ep] = calc_epr_bits(epr, 0, 0) | USB_EP_DTOG_RX; + } } return len; @@ -319,9 +330,8 @@ usb_set_configure(void) USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPRX_STAT, USB_EP_RX_VALID); ep = USB_CDC_EP_BULK_IN; - bulk_in_push_count = 0; + bulk_in_push_pos = BI_START; writel(&bulk_in_pop_flag, 0); - USB_EPR[ep] = calc_epr_bits(USB_EPR[ep], USB_EPTX_STAT, USB_EP_TX_VALID); } From 447125faae62bfad4a0e483264453a39c9ba52d8 Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Wed, 4 Oct 2023 22:26:10 -0400 Subject: [PATCH 18/21] serialqueue: Eventually time out if unable to write CANbus messages Klipper logs an error on a failed CANbus write. Unfortunately, if the bus becomes permanently disabled (eg, due to a user removing power to devices on the CANbus) then it can result in the logs filling with error messages. Permanently disable the low-level processing of messages if CANbus writes continually fail for at least 10 seconds. This avoids filling the log with redundant messages. Signed-off-by: Kevin O'Connor --- klippy/chelper/serialqueue.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/klippy/chelper/serialqueue.c b/klippy/chelper/serialqueue.c index b6500fe621d5..e6810933aabc 100644 --- a/klippy/chelper/serialqueue.c +++ b/klippy/chelper/serialqueue.c @@ -62,6 +62,7 @@ struct serialqueue { int ready_bytes, upcoming_bytes, need_ack_bytes, last_ack_bytes; uint64_t need_kick_clock; struct list_head notify_queue; + double last_write_fail_time; // Received messages struct list_head receive_queue; // Fastreader support @@ -376,8 +377,16 @@ do_write(struct serialqueue *sq, void *buf, int buflen) int ret = write(sq->serial_fd, &cf, sizeof(cf)); if (ret < 0) { report_errno("can write", ret); + double curtime = get_monotonic(); + if (!sq->last_write_fail_time) { + sq->last_write_fail_time = curtime; + } else if (curtime > sq->last_write_fail_time + 10.0) { + errorf("Halting reads due to CAN write errors."); + pollreactor_do_exit(sq->pr); + } return; } + sq->last_write_fail_time = 0.0; buf += size; buflen -= size; } From 83ef0e135ef820f73c8579069c347953b9d6d56e Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Thu, 5 Oct 2023 00:03:07 -0400 Subject: [PATCH 19/21] stm32: Make sure to limit tx during usbfs startup Wait for two tx packets before startup, and make sure one of those packets is acked before sending a third tx packet. Signed-off-by: Kevin O'Connor --- src/stm32/usbfs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stm32/usbfs.c b/src/stm32/usbfs.c index 0ed2e0ee5e5d..ad2e7b3eb56e 100644 --- a/src/stm32/usbfs.c +++ b/src/stm32/usbfs.c @@ -255,11 +255,12 @@ usb_send_bulk_in(void *data, uint_fast8_t len) uint32_t epr = USB_EPR[ep]; if (epr_is_dbuf_blocking(epr) && readl(&bulk_in_pop_flag)) { writel(&bulk_in_pop_flag, 0); - if (bipp & BI_START) { + if (unlikely(bipp & BI_START)) { // Two packets are always sent when starting in double // buffering mode, so wait for second packet before starting. if (bipp == (BI_START | 1)) { bulk_in_push_pos = 0; + writel(&bulk_in_pop_flag, USB_EP_KIND); // Dummy flag USB_EPR[ep] = calc_epr_bits(epr, USB_EPTX_STAT , USB_EP_TX_VALID); } From 7eabf02f5bd44c7966e283b67336be7ef0e8f07f Mon Sep 17 00:00:00 2001 From: ghostoverflow256 <46546505+GhostDog98@users.noreply.github.com> Date: Tue, 10 Oct 2023 04:28:49 +1100 Subject: [PATCH 20/21] config: Update printer-creality-ender5-2019.cfg to add instructions for silent boards (#6326) Recently tested on my ender 5 pro that came from creality with a v1.1.5 board. Works. Tested all endstops, motors, and heaters. Signed-off-by: Jake Aronleigh --- config/printer-creality-ender5-2019.cfg | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/printer-creality-ender5-2019.cfg b/config/printer-creality-ender5-2019.cfg index 0130321ba7bf..a1b9d15936e7 100644 --- a/config/printer-creality-ender5-2019.cfg +++ b/config/printer-creality-ender5-2019.cfg @@ -1,10 +1,12 @@ # This file contains common pin mappings for the 2019 Creality # Ender 5. To use this config, the firmware should be compiled for the -# AVR atmega1284p. +# AVR atmega1284p. This also works for the v1.1.5 silent boards. # Note, a number of Melzi boards are shipped with a bootloader that # requires the following command to flash the board: # avrdude -p atmega1284p -c arduino -b 57600 -P /dev/ttyUSB0 -U out/klipper.elf.hex +# For v1.1.5 silent boards, the following command is used: +# avrdude -p atmega1284p -c arduino -P /dev/ttyUSB0 -b 115200 -U flash:w:out/klipper.elf.hex # If the above command does not work and "make flash" does not work # then one may need to flash a bootloader to the board - see the # Klipper docs/Bootloaders.md file for more information. @@ -80,6 +82,8 @@ pin: PB4 [mcu] serial: /dev/serial/by-id/usb-1a86_USB2.0-Serial-if00-port0 +# Silent boards tend to have the exact same serial ID, except without USB2.0, using USB instead. +# e.g. /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 [printer] kinematics: cartesian From 5edc7fee7e4560806533ec6ed1550cb5faad12fc Mon Sep 17 00:00:00 2001 From: Kevin O'Connor Date: Mon, 9 Oct 2023 13:34:58 -0400 Subject: [PATCH 21/21] config: Fix trailing space in printer-creality-ender5-2019.cfg Signed-off-by: Kevin O'Connor --- config/printer-creality-ender5-2019.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/printer-creality-ender5-2019.cfg b/config/printer-creality-ender5-2019.cfg index a1b9d15936e7..cf0d4bed09a8 100644 --- a/config/printer-creality-ender5-2019.cfg +++ b/config/printer-creality-ender5-2019.cfg @@ -1,6 +1,6 @@ # This file contains common pin mappings for the 2019 Creality # Ender 5. To use this config, the firmware should be compiled for the -# AVR atmega1284p. This also works for the v1.1.5 silent boards. +# AVR atmega1284p. This also works for the v1.1.5 silent boards. # Note, a number of Melzi boards are shipped with a bootloader that # requires the following command to flash the board: