plus4的klipper版本

This commit is contained in:
whb0514
2024-09-02 13:37:34 +08:00
parent 653d7a8f6e
commit b90736975b
1006 changed files with 1195894 additions and 11114 deletions

View File

@@ -8,6 +8,7 @@ set -eu
MAIN_DIR=${PWD}
BUILD_DIR=${PWD}/ci_build
export PATH=${BUILD_DIR}/pru-gcc/bin:${PATH}
export PATH=${BUILD_DIR}/or1k-linux-musl-cross/bin:${PATH}
PYTHON=${BUILD_DIR}/python-env/bin/python
PYTHON2=${BUILD_DIR}/python2-env/bin/python

View File

@@ -16,7 +16,7 @@ mkdir -p ${BUILD_DIR} ${CACHE_DIR}
######################################################################
echo -e "\n\n=============== Install system dependencies\n\n"
PKGS="virtualenv python-dev libffi-dev build-essential"
PKGS="virtualenv python2-dev libffi-dev build-essential"
PKGS="${PKGS} gcc-avr avr-libc"
PKGS="${PKGS} libnewlib-arm-none-eabi gcc-arm-none-eabi binutils-arm-none-eabi"
PKGS="${PKGS} pv libmpfr-dev libgmp-dev libmpc-dev texinfo bison flex"
@@ -35,10 +35,10 @@ if [ ! -f ${PRU_FILE} ]; then
cd ${BUILD_DIR}
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
git clone https://github.com/dinuxbg/gnupru -b 2018.03-beta-rc3 --depth 1
git clone https://github.com/dinuxbg/gnupru -b 2023.01 --depth 1
cd gnupru
export PREFIX=${PRU_DIR}
./download-and-patch.sh 2>&1 | pv -nli 30 > ${BUILD_DIR}/gnupru-build.log
./download-and-prepare.sh 2>&1 | pv -nli 30 > ${BUILD_DIR}/gnupru-build.log
./build.sh 2>&1 | pv -nli 30 >> ${BUILD_DIR}/gnupru-build.log
cd ${BUILD_DIR}
tar cfz ${PRU_FILE} pru-gcc/
@@ -47,7 +47,21 @@ else
tar xfz ${PRU_FILE}
fi
######################################################################
# Install or1k-linux-musl toolchain
######################################################################
echo -e "\n\n=============== Install or1k-linux-musl toolchain\n\n"
TOOLCHAIN=or1k-linux-musl-cross
TOOLCHAIN_ZIP=${TOOLCHAIN}.tgz
GCC_VERSION=10
TOOLCHAIN_ZIP_V=${TOOLCHAIN}-${GCC_VERSION}.tgz
URL=https://more.musl.cc/${GCC_VERSION}/x86_64-linux-musl/
if [ ! -f ${CACHE_DIR}/${TOOLCHAIN_ZIP_V} ]; then
curl ${URL}/${TOOLCHAIN_ZIP} -o ${CACHE_DIR}/${TOOLCHAIN_ZIP_V}
fi
cd ${BUILD_DIR}
tar xf ${CACHE_DIR}/${TOOLCHAIN_ZIP_V}
######################################################################
# Create python3 virtualenv environment
######################################################################

203
scripts/dump_mcu.py Normal file
View File

@@ -0,0 +1,203 @@
#!/usr/bin/env python
# MCU flash dump assistant
#
# Copyright (C) 2022 Eric Callahan <arksine.code@gmail.com>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys
import argparse
import os
import traceback
import logging
KLIPPER_DIR = os.path.abspath(os.path.join(
os.path.dirname(__file__), "../"))
sys.path.append(os.path.join(KLIPPER_DIR, "klippy"))
import reactor
import serialhdl
import clocksync
###########################################################
#
# Helper methods
#
###########################################################
def output_line(msg):
sys.stdout.write("%s\n" % (msg,))
sys.stdout.flush()
def output(msg):
sys.stdout.write("%s" % (msg,))
sys.stdout.flush()
DUMP_CMD="debug_read order=%d addr=%d"
DUMP_RESP="debug_result"
class MCUDumpError(Exception):
pass
class MCUDump:
def __init__(self, args):
self.reactor = reactor.Reactor()
self.device = args.device
self.baud = args.baud
self.canbus_iface = args.canbus_iface
self.canbus_nodeid = args.canbus_nodeid
self.output_file = os.path.expanduser(args.outfile)
try:
self.read_start = int(args.read_start, 0)
self.read_length = int(args.read_length, 0)
except ValueError as e:
raise MCUDumpError(
"Error converting flash address: %s " % (str(e),)
)
if self.read_length <= 0:
raise MCUDumpError("Read count must be greater than 0")
self._serial = serialhdl.SerialReader(self.reactor)
self.clocksync = clocksync.ClockSync(self.reactor)
self.connect_completion = None
self.connected = False
def connect(self):
output("Connecting to MCU..")
self.connect_completion = self.reactor.completion()
self.connected = False
self.reactor.register_callback(self._do_serial_connect)
curtime = self.reactor.monotonic()
while True:
curtime = self.reactor.pause(curtime + 1.)
output(".")
if self.connect_completion.test():
self.connected = self.connect_completion.wait()
break
self.connect_completion = None
if not self.connected:
output("\n")
raise MCUDumpError("Unable to connect to MCU")
output_line("Connected")
msgparser = self._serial.get_msgparser()
mcu_id = msgparser.get_constant("MCU")
freq = msgparser.get_constant("CLOCK_FREQ")
output_line("MCU type: %s" % (mcu_id,))
output_line("Frequency: %s\n" % (freq,))
def _do_serial_connect(self, eventtime):
endtime = eventtime + 60.
while True:
try:
if self.canbus_iface is not None:
self._serial.connect_canbus(
self.device, self.canbus_nodeid, self.canbus_iface
)
elif (
self.device.startswith("/dev/rpmsg_") or
self.device.startswith("/tmp/")
):
self._serial.connect_pipe(self.device)
else:
self._serial.connect_uart(self.device, self.baud)
self.clocksync.connect(self._serial)
except Exception:
curtime = self.reactor.monotonic()
if curtime > endtime:
self.connect_completion.complete(False)
return
output("Connection Error, retrying..")
self._serial.disconnect()
self.reactor.pause(curtime + 2.)
else:
break
self.connect_completion.complete(True)
def disconnect(self):
if not self.connected:
return
self._serial.disconnect()
self.connected = False
def _dump_flash(self):
addr = self.read_start
count = self.read_length
order = [2, 0, 1, 0][(addr | count) & 3]
bsize = 1 << order
# Query data from mcu
output_line(
"Reading %d bytes from flash, start address 0x%x\n"
% (count, addr)
)
output("[")
bytes_read = last_reported_pct = 0
vals = []
for i in range((count + bsize - 1) >> order):
caddr = addr + (i << order)
cmd = DUMP_CMD % (order, caddr)
params = self._serial.send_with_response(cmd, DUMP_RESP)
vals.append(params['val'])
bytes_read += bsize
pct = int(bytes_read / float(count) * 100 + .5)
diff = (pct - last_reported_pct) // 2
if diff:
last_reported_pct = pct
output("#" * diff)
output_line("]\n")
# Convert to byte format
data = bytearray()
for val in vals:
for b in range(bsize):
data.append((val >> (8 * b)) & 0xff)
data = data[:count]
with open(self.output_file, "wb") as f:
f.write(data)
output_line("Wrote %d bytes to '%s'" % (len(data), self.output_file))
def _run_dump_task(self, eventtime):
self.connect()
self._dump_flash()
self.reactor.end()
def run(self):
self.reactor.register_callback(self._run_dump_task)
try:
self.reactor.run()
finally:
self.disconnect()
self.reactor.finalize()
def main():
parser = argparse.ArgumentParser(description="MCU Flash Dump Utility")
parser.add_argument(
"-b", "--baud", metavar="<baud rate>", type=int,
default=250000, help="Baud Rate")
parser.add_argument(
"-c", "--canbus_iface", metavar="<canbus iface>", default=None,
help="Use CAN bus interface; <device> is the chip UUID")
parser.add_argument(
"-i", "--canbus_nodeid", metavar="<canbus nodeid>", type=int,
default=64, help="The CAN nodeid to use (default 64)")
parser.add_argument(
"-s", "--read_start", metavar="<read start>", default="0x0",
help="Flash address to start reading")
parser.add_argument(
"-l", "--read_length", metavar="<read length>", default="0x400",
help="Number of bytes to read")
parser.add_argument(
"device", metavar="<device>", help="Device Serial Port")
parser.add_argument(
"outfile", metavar="<outfile>",
help="Path to output file")
args = parser.parse_args()
logging.basicConfig(level=logging.CRITICAL)
try:
mcudump = MCUDump(args)
mcudump.run()
except Exception as e:
output_line("\nMCU Dump Error: %s" % (str(e),))
traceback.print_exc(file=sys.stdout)
sys.exit(-1)
output_line("MCU Dump Complete")
if __name__ == "__main__":
main()

102
scripts/flash-ar100.py Normal file
View File

@@ -0,0 +1,102 @@
#!/usr/bin/env python3
# This file may be distributed under the terms of the GNU GPLv3 license.
import mmap
import argparse
import sys
FW_START = 0x00004000
FW_BASE = 0x00040000 + FW_START
FW_LIMIT = 0x00054000
FW_SIZE = FW_LIMIT - FW_BASE
EXCEPTIONS_BASE = 0x00040000
EXCEPTIONS_LIMIT = 0x00042000
EXCEPTIONS_SIZE = EXCEPTIONS_LIMIT - EXCEPTIONS_BASE
EXCEPTIONS_JUMP = FW_START # All exceptions reset program
NR_OF_EXCEPTIONS = 14
R_CPU_CFG_PAGE_BASE = 0x01F01000
R_CPU_CFG_PAGE_LIMIT = 0x01F02000
R_CPU_CFG_SIZE = R_CPU_CFG_PAGE_LIMIT - R_CPU_CFG_PAGE_BASE
R_CPU_CFG_OFFSET = 0xC00
R_CPU_CLK_OFFSET = 0x400
parser = argparse.ArgumentParser(description='Flash and reset SRAM A2 of A64')
parser.add_argument('filename', nargs='?', help='binary file to write')
parser.add_argument('--reset', action='store_true', help='reset the AR100')
parser.add_argument('--halt', action='store_true', help='Halt the AR100')
parser.add_argument('--bl31', action='store_true', help='write bl31')
args = parser.parse_args()
def write_exception_vectors():
print("Writing exception vectors")
with open("/dev/mem", "w+b") as f:
exc = mmap.mmap(f.fileno(),
length=EXCEPTIONS_SIZE,
offset=EXCEPTIONS_BASE)
for i in range(NR_OF_EXCEPTIONS):
add = i * 0x100
exc[add:add + 4] = ((EXCEPTIONS_JUMP - add) >> 2).to_bytes(
4, byteorder='little')
exc.close()
def assert_deassert_reset(ass):
with open("/dev/mem", "w+b") as f:
r_cpucfg = mmap.mmap(f.fileno(),
length=R_CPU_CFG_SIZE,
offset=R_CPU_CFG_PAGE_BASE)
if ass:
r_cpucfg[R_CPU_CFG_OFFSET] &= ~0x01
if r_cpucfg[R_CPU_CFG_OFFSET] & 0x01:
print("failed to assert reset")
else:
r_cpucfg[R_CPU_CFG_OFFSET] |= 0x01
if not (r_cpucfg[R_CPU_CFG_OFFSET] & 0x01):
print("failed to deassert reset")
r_cpucfg.close()
def write_file(filename):
with open(filename, "r+b") as fw:
data = fw.read()
if len(data) > FW_SIZE:
print("File does not fit in memory")
sys.exit(1)
print("Writing file to SRAM A2")
with open("/dev/mem", "w+b") as f:
sram_a2 = mmap.mmap(f.fileno(), length=FW_SIZE, offset=FW_BASE)
sram_a2[0:len(data)] = data
sram_a2.close()
def clear_magic_word():
with open("/dev/mem", "w+b") as f:
sram_a2 = mmap.mmap(f.fileno(), length=FW_SIZE, offset=FW_BASE)
sram_a2[0] = 0x0
sram_a2.close()
if args.reset:
print("Resetting AR100")
assert_deassert_reset(1)
assert_deassert_reset(0)
sys.exit(0)
if args.filename:
if args.bl31:
print("writing bl31")
assert_deassert_reset(1)
write_file(args.filename)
else:
assert_deassert_reset(1)
write_exception_vectors()
write_file(args.filename)
assert_deassert_reset(0)
if args.halt:
print("Halting AR100")
assert_deassert_reset(1)
clear_magic_word()

View File

@@ -7,10 +7,17 @@ if [ "$EUID" -ne 0 ]; then
fi
set -e
# Setting build output directory
if [ -z "${1}" ]; then
out='out'
else
out=${1}
fi
# Install new micro-controller code
echo "Installing micro-controller code to /usr/local/bin/"
rm -f /usr/local/bin/klipper_mcu
cp out/klipper.elf /usr/local/bin/klipper_mcu
cp ${out}/klipper.elf /usr/local/bin/klipper_mcu
sync
# Restart (if system install script present)
@@ -24,3 +31,8 @@ if [ -f /etc/init.d/klipper_mcu ]; then
echo "Attempting host MCU restart..."
service klipper_mcu restart
fi
if [ -f /etc/systemd/system/klipper-mcu.service ]; then
echo "Attempting host MCU restart..."
systemctl restart klipper-mcu
fi

View File

@@ -10,6 +10,7 @@ KLIPPER_BIN_DEFAULT=$KLIPPER_BIN
KLIPPER_DICT_DEFAULT="${SRCDIR}/out/klipper.dict"
SPI_FLASH="${SRCDIR}/scripts/spi_flash/spi_flash.py"
BAUD_ARG=""
CHECK_ARG=""
# Force script to exit if an error occurs
set -e
@@ -17,7 +18,7 @@ print_help_message()
{
echo "SD Card upload utility for Klipper"
echo
echo "usage: flash_sdcard.sh [-h] [-l] [-b <baud>] [-f <firmware>] [-d <dictionary>]"
echo "usage: flash_sdcard.sh [-h] [-l] [-c] [-b <baud>] [-f <firmware>] [-d <dictionary>]"
echo " <device> <board>"
echo
echo "positional arguments:"
@@ -27,13 +28,14 @@ print_help_message()
echo "optional arguments:"
echo " -h show this message"
echo " -l list available boards"
echo " -c run flash check/verify only (skip upload)"
echo " -b <baud> serial baud rate (default is 250000)"
echo " -f <firmware> path to klipper.bin"
echo " -d <dictionary> path to klipper.dict for firmware validation"
}
# Parse command line "optional args"
while getopts "hlb:f:d:" arg; do
while getopts "hlcb:f:d:" arg; do
case $arg in
h)
print_help_message
@@ -43,6 +45,7 @@ while getopts "hlb:f:d:" arg; do
${KLIPPY_ENV} ${SPI_FLASH} -l
exit 0
;;
c) CHECK_ARG="-c";;
b) BAUD_ARG="-b ${OPTARG}";;
f) KLIPPER_BIN=$OPTARG;;
d) KLIPPER_DICT=$OPTARG;;
@@ -82,4 +85,4 @@ fi
# Run Script
echo "Flashing ${KLIPPER_BIN} to ${DEVICE}"
${KLIPPY_ENV} ${SPI_FLASH} ${BAUD_ARG} ${KLIPPER_DICT} ${DEVICE} ${BOARD} ${KLIPPER_BIN}
${KLIPPY_ENV} ${SPI_FLASH} ${CHECK_ARG} ${BAUD_ARG} ${KLIPPER_DICT} ${DEVICE} ${BOARD} ${KLIPPER_BIN}

View File

@@ -193,6 +193,7 @@ def call_picoboot(bus, addr, binfile, sudo):
# Flash via Klipper modified "picoboot"
def flash_picoboot(device, binfile, sudo):
ttyname, serbypath = translate_serial_to_tty(device)
buspath, devpath = translate_serial_to_usb_path(device)
# We need one level up to get access to busnum/devnum files
usbdir = os.path.dirname(devpath)
@@ -202,7 +203,10 @@ def flash_picoboot(device, binfile, sudo):
bus = f.read().strip()
with open(usbdir + "/devnum") as f:
addr = f.read().strip()
call_picoboot(bus, addr, binfile, sudo)
if detect_canboot(devpath):
call_flashcan(serbypath, binfile)
else:
call_picoboot(bus, addr, binfile, sudo)
######################################################################
@@ -336,11 +340,14 @@ def flash_rp2040(options, binfile):
sys.exit(-1)
MCUTYPES = {
'sam3': flash_atsam3, 'sam4': flash_atsam4, 'samd': flash_atsamd,
'same70': flash_atsam4, 'lpc176': flash_lpc176x, 'stm32f103': flash_stm32f1,
'sam3': flash_atsam3, 'sam4': flash_atsam4, 'same70': flash_atsam4,
'samd': flash_atsamd, 'same5': flash_atsamd,
'lpc176': flash_lpc176x, 'stm32f103': flash_stm32f1,
'stm32f4': flash_stm32f4, 'stm32f042': flash_stm32f4,
'stm32f072': flash_stm32f4, 'stm32g0b1': flash_stm32f4,
'stm32h7': flash_stm32f4, 'rp2040': flash_rp2040
'stm32f070': flash_stm32f4, 'stm32f072': flash_stm32f4,
'stm32g0b1': flash_stm32f4, 'stm32f7': flash_stm32f4,
'stm32h7': flash_stm32f4, 'stm32l4': flash_stm32f4,
'stm32g4': flash_stm32f4, 'rp2040': flash_rp2040,
}

View File

@@ -17,35 +17,50 @@ MAX_TITLE_LENGTH=65
def parse_log(logname, opts):
with open(logname) as f:
for header in f:
if not header.startswith('#'):
if header.startswith('#'):
continue
if header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
# Processed power spectral density file
break
if not header.startswith('freq,psd_x,psd_y,psd_z,psd_xyz'):
# Raw accelerometer data
return np.loadtxt(logname, comments='#', delimiter=',')
# Power spectral density data or shaper calibration data
opts.error("File %s does not contain raw accelerometer data and therefore "
"is not supported by graph_accelerometer.py script. Please use "
"calibrate_shaper.py script to process it instead." % (logname,))
# Parse power spectral density data
data = np.loadtxt(logname, skiprows=1, comments='#', delimiter=',')
calibration_data = shaper_calibrate.CalibrationData(
freq_bins=data[:,0], psd_sum=data[:,4],
psd_x=data[:,1], psd_y=data[:,2], psd_z=data[:,3])
calibration_data.set_numpy(np)
return calibration_data
######################################################################
# Raw accelerometer graphing
######################################################################
def plot_accel(data, logname):
first_time = data[0, 0]
times = data[:,0] - first_time
def plot_accel(datas, lognames):
fig, axes = matplotlib.pyplot.subplots(nrows=3, sharex=True)
axes[0].set_title("\n".join(wrap("Accelerometer data (%s)" % (logname,),
MAX_TITLE_LENGTH)))
axes[0].set_title("\n".join(wrap(
"Accelerometer data (%s)" % (', '.join(lognames)), MAX_TITLE_LENGTH)))
axis_names = ['x', 'y', 'z']
for data, logname in zip(datas, lognames):
if isinstance(data, shaper_calibrate.CalibrationData):
raise error("Cannot plot raw accelerometer data using the processed"
" resonances, raw_data input is required")
first_time = data[0, 0]
times = data[:,0] - first_time
for i in range(len(axis_names)):
avg = data[:,i+1].mean()
adata = data[:,i+1] - data[:,i+1].mean()
ax = axes[i]
label = '\n'.join(wrap(logname, 60)) + ' (%+.3f mm/s^2)' % (-avg,)
ax.plot(times, adata, alpha=0.8, label=label)
axes[-1].set_xlabel('Time (s)')
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('x-small')
for i in range(len(axis_names)):
avg = data[:,i+1].mean()
adata = data[:,i+1] - data[:,i+1].mean()
ax = axes[i]
ax.plot(times, adata, alpha=0.8)
ax.grid(True)
ax.set_ylabel('%s accel (%+.3f)\n(mm/s^2)' % (axis_names[i], -avg))
axes[-1].set_xlabel('Time (%+.3f)\n(s)' % (-first_time,))
ax.legend(loc='best', prop=fontP)
ax.set_ylabel('%s accel' % (axis_names[i],))
fig.tight_layout()
return fig
@@ -56,10 +71,15 @@ def plot_accel(data, logname):
# Calculate estimated "power spectral density"
def calc_freq_response(data, max_freq):
if isinstance(data, shaper_calibrate.CalibrationData):
return data
helper = shaper_calibrate.ShaperCalibrate(printer=None)
return helper.process_accelerometer_data(data)
def calc_specgram(data, axis):
if isinstance(data, shaper_calibrate.CalibrationData):
raise error("Cannot calculate the spectrogram using the processed"
" resonances, raw_data input is required")
N = data.shape[0]
Fs = N / (data[-1,0] - data[0,0])
# Round up to a power of 2 for faster FFT
@@ -235,9 +255,7 @@ def main():
# Draw graph
if options.raw:
if len(args) > 1:
opts.error("Only 1 input is supported in raw mode")
fig = plot_accel(datas[0], args[0])
fig = plot_accel(datas, args)
elif options.specgram:
if len(args) > 1:
opts.error("Only 1 input is supported in specgram mode")

View File

@@ -181,14 +181,48 @@ def plot_system(data):
ax1.grid(True)
return fig
def plot_frequency(data, mcu):
def plot_mcu_frequencies(data):
all_keys = {}
for d in data:
all_keys.update(d)
one_mcu = mcu is not None
graph_keys = { key: ([], []) for key in all_keys
if (key in ("freq", "adj") or (not one_mcu and (
key.endswith(":freq") or key.endswith(":adj")))) }
if (key in ("freq", "adj")
or (key.endswith(":freq") or key.endswith(":adj"))) }
for d in data:
st = datetime.datetime.utcfromtimestamp(d['#sampletime'])
for key, (times, values) in graph_keys.items():
val = d.get(key)
if val not in (None, '0', '1'):
times.append(st)
values.append(float(val))
est_mhz = { key: round((sum(values)/len(values)) / 1000000.)
for key, (times, values) in graph_keys.items() }
# Build plot
fig, ax1 = matplotlib.pyplot.subplots()
ax1.set_title("MCU frequencies")
ax1.set_xlabel('Time')
ax1.set_ylabel('Microsecond deviation')
for key in sorted(graph_keys):
times, values = graph_keys[key]
mhz = est_mhz[key]
label = "%s(%dMhz)" % (key, mhz)
hz = mhz * 1000000.
ax1.plot_date(times, [(v - hz)/mhz for v in values], '.', label=label)
fontP = matplotlib.font_manager.FontProperties()
fontP.set_size('x-small')
ax1.legend(loc='best', prop=fontP)
ax1.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%H:%M'))
ax1.yaxis.set_major_formatter(matplotlib.ticker.FormatStrFormatter('%d'))
ax1.grid(True)
return fig
def plot_mcu_frequency(data, mcu):
all_keys = {}
for d in data:
all_keys.update(d)
graph_keys = { key: ([], []) for key in all_keys
if key in ("freq", "adj") }
for d in data:
st = datetime.datetime.utcfromtimestamp(d['#sampletime'])
for key, (times, values) in graph_keys.items():
@@ -199,10 +233,7 @@ def plot_frequency(data, mcu):
# Build plot
fig, ax1 = matplotlib.pyplot.subplots()
if one_mcu:
ax1.set_title("MCU '%s' frequency" % (mcu,))
else:
ax1.set_title("MCU frequency")
ax1.set_title("MCU '%s' frequency" % (mcu,))
ax1.set_xlabel('Time')
ax1.set_ylabel('Frequency')
for key in sorted(graph_keys):
@@ -286,7 +317,10 @@ def main():
if options.heater is not None:
fig = plot_temperature(data, options.heater)
elif options.frequency:
fig = plot_frequency(data, options.mcu)
if options.mcu is not None:
fig = plot_mcu_frequency(data, options.mcu)
else:
fig = plot_mcu_frequencies(data)
elif options.system:
fig = plot_system(data)
else:

View File

@@ -20,7 +20,7 @@ install_packages()
PKGLIST="${PKGLIST} avrdude gcc-avr binutils-avr avr-libc"
# ARM chip installation and building
PKGLIST="${PKGLIST} stm32flash libnewlib-arm-none-eabi"
PKGLIST="${PKGLIST} gcc-arm-none-eabi binutils-arm-none-eabi libusb-1.0"
PKGLIST="${PKGLIST} gcc-arm-none-eabi binutils-arm-none-eabi libusb-1.0 pkg-config"
# Update system package info
report_status "Running apt-get update..."

View File

@@ -0,0 +1,102 @@
#!/bin/bash
# This script installs Klipper on an Ubuntu 22.04 ("Jammy") machine
PYTHONDIR="${HOME}/klippy-env"
SYSTEMDDIR="/etc/systemd/system"
KLIPPER_USER=$USER
KLIPPER_GROUP=$KLIPPER_USER
# Step 1: Install system packages
install_packages()
{
# Packages for python cffi
PKGLIST="virtualenv python3-dev libffi-dev build-essential"
# kconfig requirements
PKGLIST="${PKGLIST} libncurses-dev"
# hub-ctrl
PKGLIST="${PKGLIST} libusb-dev"
# AVR chip installation and building
PKGLIST="${PKGLIST} avrdude gcc-avr binutils-avr avr-libc"
# ARM chip installation and building
PKGLIST="${PKGLIST} stm32flash dfu-util libnewlib-arm-none-eabi"
PKGLIST="${PKGLIST} gcc-arm-none-eabi binutils-arm-none-eabi libusb-1.0"
# Update system package info
report_status "Running apt-get update..."
sudo apt-get update
# Install desired packages
report_status "Installing packages..."
sudo apt-get install --yes ${PKGLIST}
}
# Step 2: Create python virtual environment
create_virtualenv()
{
report_status "Updating python virtual environment..."
# Create virtualenv if it doesn't already exist
[ ! -d ${PYTHONDIR} ] && virtualenv -p python3 ${PYTHONDIR}
# Install/update dependencies
${PYTHONDIR}/bin/pip install -r ${SRCDIR}/scripts/klippy-requirements.txt
}
# Step 3: Install startup script
install_script()
{
# Create systemd service file
KLIPPER_LOG=/tmp/klippy.log
report_status "Installing system start script..."
sudo /bin/sh -c "cat > $SYSTEMDDIR/klipper.service" << EOF
#Systemd service file for klipper
[Unit]
Description=Starts klipper on startup
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=$KLIPPER_USER
RemainAfterExit=yes
ExecStart=${PYTHONDIR}/bin/python ${SRCDIR}/klippy/klippy.py ${HOME}/printer.cfg -l ${KLIPPER_LOG}
EOF
# Use systemctl to enable the klipper systemd service script
sudo systemctl enable klipper.service
}
# Step 4: Start host software
start_software()
{
report_status "Launching Klipper host software..."
sudo systemctl start klipper
}
# Helper functions
report_status()
{
echo -e "\n\n###### $1"
}
verify_ready()
{
if [ "$EUID" -eq 0 ]; then
echo "This script must not run as root"
exit -1
fi
}
# Force script to exit if an error occurs
set -e
# Find SRCDIR from the pathname of this script
SRCDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
# Run installation steps defined above
verify_ready
install_packages
create_virtualenv
install_script
start_software

View File

@@ -1,78 +0,0 @@
#!/bin/sh
# System startup script to start the MCU Linux firmware
### BEGIN INIT INFO
# Provides: klipper_mcu
# Required-Start: $local_fs
# Required-Stop:
# Default-Start: 3 4 5
# Default-Stop: 0 1 2 6
# Short-Description: Klipper_MCU daemon
# Description: Starts the MCU for Klipper.
### END INIT INFO
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DESC="klipper_mcu startup"
NAME="klipper_mcu"
KLIPPER_HOST_MCU=/usr/local/bin/klipper_mcu
KLIPPER_HOST_ARGS="-r"
PIDFILE=/var/run/klipper_mcu.pid
. /lib/lsb/init-functions
mcu_host_stop()
{
# Shutdown existing Klipper instance (if applicable). The goal is to
# put the GPIO pins in a safe state.
if [ -c /tmp/klipper_host_mcu ]; then
log_daemon_msg "Attempting to shutdown host mcu..."
set -e
( echo "FORCE_SHUTDOWN" > /tmp/klipper_host_mcu ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
sleep 1
( echo "FORCE_SHUTDOWN" > /tmp/klipper_host_mcu ) 2> /dev/null || ( log_action_msg "Firmware busy! Please shutdown Klipper and then retry." && exit 1 )
sleep 1
set +e
fi
log_daemon_msg "Stopping klipper host mcu" $NAME
killproc -p $PIDFILE $KLIPPER_HOST_MCU
}
mcu_host_start()
{
[ -x $KLIPPER_HOST_MCU ] || return
if [ -c /tmp/klipper_host_mcu ]; then
mcu_host_stop
fi
log_daemon_msg "Starting klipper MCU" $NAME
start-stop-daemon --start --quiet --exec $KLIPPER_HOST_MCU \
--background --pidfile $PIDFILE --make-pidfile \
-- $KLIPPER_HOST_ARGS
log_end_msg $?
}
case "$1" in
start)
mcu_host_start
;;
stop)
mcu_host_stop
;;
restart)
$0 stop
$0 start
;;
reload|force-reload)
log_daemon_msg "Reloading configuration not supported" $NAME
log_end_msg 1
;;
status)
status_of_proc -p $PIDFILE $KLIPPER_HOST_MCU $NAME && exit 0 || exit $?
;;
*) log_action_msg "Usage: /etc/init.d/klipper_mcu {start|stop|status|restart|reload|force-reload}"
exit 2
;;
esac
exit 0

View File

@@ -0,0 +1,21 @@
#Systemd klipper linux mcu Service
[Unit]
Description=Starts the MCU Linux firmware for klipper on startup
Documentation=https://www.klipper3d.org/RPi_microcontroller.html
Before=klipper.service
ConditionFileIsExecutable=/usr/local/bin/klipper_mcu
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
Environment=KLIPPER_HOST_MCU_SERIAL=/tmp/klipper_host_mcu
RemainAfterExit=yes
ExecStart=/usr/local/bin/klipper_mcu -r -I ${KLIPPER_HOST_MCU_SERIAL}
ExecStop=sh -c 'echo "FORCE_SHUTDOWN" > ${KLIPPER_HOST_MCU_SERIAL}'
ExecStop=sleep 1
TimeoutStopSec=2
Restart=always
RestartSec=5

View File

@@ -4,7 +4,7 @@
# pip install -r klippy-requirements.txt
cffi==1.14.6
pyserial==3.4
greenlet==1.1.2
greenlet==2.0.2
Jinja2==2.11.3
python-can==3.3.4
markupsafe==1.1.1

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# Script to extract config and shutdown information file a klippy.log file
#
# Copyright (C) 2017 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, re, collections, ast
import sys, re, collections, ast, itertools
def format_comment(line_num, line):
return "# %6d: %s" % (line_num, line)
@@ -40,9 +40,10 @@ class GatherConfig:
if comment is not None:
self.comments.append(comment)
def write_file(self):
f = open(self.filename, 'wb')
f.write('\n'.join(self.comments + self.config_lines).strip() + '\n')
f.close()
lines = itertools.chain(self.comments, self.config_lines)
lines = ('%s\n' % l for l in lines)
with open(self.filename, 'wt') as f:
f.writelines(lines)
######################################################################
@@ -93,7 +94,7 @@ class TMCUartHelper:
return
# Convert data into a long integer for easy manipulation
mval = pos = 0
for d in bytearray(data):
for d in data:
mval |= d << pos
pos += 8
# Extract register value
@@ -110,7 +111,7 @@ class TMCUartHelper:
return
# Convert data into a long integer for easy manipulation
mval = pos = 0
for d in bytearray(data):
for d in data:
mval |= d << pos
pos += 8
# Extract register value
@@ -216,10 +217,10 @@ clock_r = re.compile(r"^clocksync state: mcu_freq=(?P<freq>[0-9]+) .*"
+ r" (?P<sc>[0-9]+) (?P<f>[^ ]+)\)")
repl_seq_r = re.compile(r": seq: 1" + shortseq_s)
clock_s = r"(?P<clock>[0-9]+)"
repl_clock_r = re.compile(r"clock=" + clock_s + r"(?: |$)")
repl_clock_r = re.compile(r"clock=" + clock_s)
repl_uart_r = re.compile(r"tmcuart_(?:response|send) oid=[0-9]+"
+ r" (?:read|write)=(?P<msg>(?:'[^']*'"
+ r'|"[^"]*"))(?: |$)')
+ r" (?:read|write)=b?(?P<msg>(?:'[^']*'"
+ r'|"[^"]*"))')
# MCU shutdown message parsing
class MCUStream:
@@ -240,12 +241,13 @@ class MCUStream:
if seq is not None:
line = repl_seq_r.sub(r"\g<0>(%d)" % (seq,), line)
def clock_update(m):
return m.group(0).rstrip() + "(%.6f) " % (
return m.group(0).rstrip() + "(%.6f)" % (
self.trans_clock(int(m.group('clock')), ts),)
line = repl_clock_r.sub(clock_update, line)
def uart_update(m):
msg = TMCUartHelper().parse_msg(ast.literal_eval(m.group('msg')))
return m.group(0).rstrip() + "%s " % (msg,)
msg = ast.literal_eval('b' + m.group('msg'))
msg = TMCUartHelper().parse_msg(msg)
return m.group(0).rstrip() + msg
line = repl_uart_r.sub(uart_update, line)
if self.name != 'mcu':
line = "mcu '%s': %s" % (self.name, line)
@@ -382,10 +384,9 @@ class GCodeStream:
def get_lines(self):
# Produce output gcode stream
if self.gcode_stream:
data = [ast.literal_eval(gc) for gc in self.gcode_commands]
f = open(self.gcode_filename, 'wb')
f.write(self.gcode_state + ''.join(data))
f.close()
data = (ast.literal_eval(gc) for gc in self.gcode_commands)
with open(self.gcode_filename, 'wt') as f:
f.write(self.gcode_state + ''.join(data))
return self.gcode_stream
api_cmd_r = re.compile(r"^Received " + time_s + r": \{.*\}$")
@@ -500,8 +501,13 @@ class StatsStream:
if info[0] is not None and info[0] >= min_stream_ts - 5.:
del self.stats_stream[:i]
break
# Find the first stats timestamp
last_ts = min_stream_ts
for ts, line_num, line in self.stats_stream:
if ts is not None:
last_ts = ts
break
# Improve accuracy of stats timestamps
last_ts = self.stats_stream[0][0]
for i, (ts, line_num, line) in enumerate(self.stats_stream):
if ts is not None:
last_ts = self.check_stats_seq(ts, line)
@@ -560,10 +566,10 @@ class GatherShutdown:
# Produce output sorted by timestamp
out = [i for s in streams for i in s]
out.sort()
out = [i[2] for i in out]
f = open(self.filename, 'wb')
f.write('\n'.join(self.comments + out))
f.close()
lines = itertools.chain(self.comments, (i[2] for i in out))
lines = ('%s\n' % l for l in lines)
with open(self.filename, 'wt') as f:
f.writelines(lines)
######################################################################
@@ -577,29 +583,31 @@ def main():
handler = None
recent_lines = collections.deque([], 200)
# Parse log file
f = open(logname, 'rb')
for line_num, line in enumerate(f):
line = line.rstrip()
line_num += 1
recent_lines.append((line_num, line))
if handler is not None:
ret = handler.add_line(line_num, line)
if ret:
continue
recent_lines.clear()
handler = None
if line.startswith('Git version'):
last_git = format_comment(line_num, line)
elif line.startswith('Start printer at'):
last_start = format_comment(line_num, line)
elif line == '===== Config file =====':
handler = GatherConfig(configs, line_num, recent_lines, logname)
handler.add_comment(last_git)
handler.add_comment(last_start)
elif 'shutdown: ' in line or line.startswith('Dumping '):
handler = GatherShutdown(configs, line_num, recent_lines, logname)
handler.add_comment(last_git)
handler.add_comment(last_start)
with open(logname, 'rt') as f:
for line_num, line in enumerate(f):
line = line.rstrip()
line_num += 1
recent_lines.append((line_num, line))
if handler is not None:
ret = handler.add_line(line_num, line)
if ret:
continue
recent_lines.clear()
handler = None
if line.startswith('Git version'):
last_git = format_comment(line_num, line)
elif line.startswith('Start printer at'):
last_start = format_comment(line_num, line)
elif line == '===== Config file =====':
handler = GatherConfig(configs, line_num,
recent_lines, logname)
handler.add_comment(last_git)
handler.add_comment(last_start)
elif 'shutdown: ' in line or line.startswith('Dumping '):
handler = GatherShutdown(configs, line_num,
recent_lines, logname)
handler.add_comment(last_git)
handler.add_comment(last_start)
if handler is not None:
handler.finalize()
# Write found config files

View File

@@ -23,7 +23,7 @@ def main(argv):
help='Name of distro this package is intended for'
)
args = p.parse_args()
print(util.get_git_version(from_file=False),
print(util.get_git_version(from_file=False)["version"],
args.distroname.replace(' ', ''), sep='-')

View File

@@ -105,6 +105,97 @@ class GenIntegral:
return data
AHandlers["integral"] = GenIntegral
# Calculate a pointwise 2-norm of several datasets (e.g. compute velocity or
# accel from its x, y,... components)
class GenNorm2:
ParametersMin = 2
ParametersMax = 3
DataSets = [
('norm2(<dataset1>,<dataset2>)',
'pointwise 2-norm of dataset1 and dataset2'),
('norm2(<dataset1>,<dataset2>,<dataset3>)',
'pointwise 2-norm of 3 datasets'),
]
def __init__(self, amanager, name_parts):
self.amanager = amanager
self.datasets = []
self.datasets.append(name_parts[1])
self.datasets.append(name_parts[2])
if len(name_parts) == 4:
self.datasets.append(name_parts[3])
for dataset in self.datasets:
amanager.setup_dataset(dataset)
def get_label(self):
label = self.amanager.get_label(self.datasets[0])
units = label['units']
datas = ['position', 'velocity', 'acceleration']
data_name = ''
for d in datas:
if d in label['label']:
data_name = d
break
lname = ''
for d in self.datasets:
l = self.amanager.get_label(d)['label']
for r in datas:
l = l.replace(r, '').strip()
if lname:
lname += '+'
lname += l
lname += ' ' + data_name + ' norm2'
return {'label': lname, 'units': units}
def generate_data(self):
seg_time = self.amanager.get_segment_time()
data = []
for dataset in self.datasets:
data.append(self.amanager.get_datasets()[dataset])
res = [0.] * len(data[0])
for i in range(len(data[0])):
norm2 = 0.
for dataset in data:
norm2 += dataset[i] * dataset[i]
res[i] = math.sqrt(norm2)
return res
AHandlers["norm2"] = GenNorm2
class GenSmoothed:
ParametersMin = 1
ParametersMax = 2
DataSets = [
('smooth(<dataset>)', 'Generate moving weighted average of a dataset'),
('smooth(<dataset>,<smooth_time>)',
'Generate moving weighted average of a dataset with a given'
' smoothing time that defines the window size'),
]
def __init__(self, amanager, name_parts):
self.amanager = amanager
self.source = name_parts[1]
amanager.setup_dataset(self.source)
self.smooth_time = 0.01
if len(name_parts) > 2:
self.smooth_time = float(name_parts[2])
def get_label(self):
label = self.amanager.get_label(self.source)
return {'label': 'Smoothed ' + label['label'], 'units': label['units']}
def generate_data(self):
seg_time = self.amanager.get_segment_time()
src = self.amanager.get_datasets()[self.source]
n = len(src)
data = [0.] * n
hst = 0.5 * self.smooth_time
seg_half_len = round(hst / seg_time)
inv_norm = 1. / sum([min(k + 1, seg_half_len + seg_half_len - k)
for k in range(2 * seg_half_len)])
for i in range(n):
j = max(0, i - seg_half_len)
je = min(n, i + seg_half_len)
avg_val = 0.
for k, v in enumerate(src[j:je]):
avg_val += v * min(k + 1, seg_half_len + seg_half_len - k)
data[i] = avg_val * inv_norm
return data
AHandlers["smooth"] = GenSmoothed
# Calculate a kinematic stepper position from the toolhead requested position
class GenKinematicPosition:
ParametersMin = ParametersMax = 1

View File

@@ -217,6 +217,8 @@ class HandleStepQ:
inv_freq = tdiff / cdiff
step_dist = jmsg['step_distance']
step_pos = jmsg['start_position']
if not step_data[0][0]:
step_data[0] = (0., step_pos, step_pos)
for interval, raw_count, add in jmsg['data']:
qs_dist = step_dist
count = raw_count
@@ -316,6 +318,8 @@ class HandleStepPhase:
if cdiff:
inv_freq = tdiff / cdiff
step_pos = jmsg['start_mcu_position']
if not step_data[0][0]:
step_data[0] = (0., step_pos)
for interval, raw_count, add in jmsg['data']:
qs_dist = 1
count = raw_count

125
scripts/parsecandump.py Normal file
View File

@@ -0,0 +1,125 @@
#!/usr/bin/python3
# Check for out of order timestamps in the output of candump
#
# Copyright (C) 2023 Kevin O'Connor <kevin@koconnor.net>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys, os, optparse
def import_msgproto():
global msgproto
# Load msgproto.py module
kdir = os.path.join(os.path.dirname(__file__), '..', 'klippy')
sys.path.append(kdir)
import msgproto
def read_dictionary(filename):
dfile = open(filename, 'rb')
dictionary = dfile.read()
dfile.close()
return dictionary
def report(msg, line_info, name="", is_err=False):
line_number, line_time = line_info
warn = ""
if is_err:
warn = " WARN"
sys.stdout.write("%04d:%010.6f:%s%s %s\n"
% (line_number, line_time, name, warn, msg))
class canscan:
def __init__(self, name, mp):
self.name = name
self.mp = mp
self.data = bytearray()
self.need_scan = False
def handle_data(self, line_info, line, newdata):
data = self.data
data += bytearray(newdata)
while 1:
if self.need_scan:
drop = len(data)
syncpos = data.find(msgproto.MESSAGE_SYNC)
if syncpos >= 0:
drop = syncpos + 1
self.need_scan = False
disc = ["%02X" % (d,) for d in data[:drop]]
report("Discarding %d (%s)" % (drop, " ".join(disc)),
line_info, self.name, is_err=True)
data[:drop] = []
if not data:
break
l = self.mp.check_packet(data)
if l == 0:
break
if l < 0:
report("Invalid data: %s" % (line.strip(),),
line_info, self.name, is_err=True)
self.need_scan = True
continue
if l == 5:
report("Ack %02x" % (data[1],), line_info, self.name)
else:
msgs = self.mp.dump(data[:l])
report("%d: %s" % (l, ', '.join(msgs)), line_info, self.name)
data[:l] = []
def read_candump(canfile, canid, dictionary):
mp = msgproto.MessageParser()
mp.process_identify(dictionary, decompress=False)
rxid = "%03X" % (canid | 1,)
txid = "%03X" % (canid & ~1,)
handlers = {rxid: canscan("RX", mp), txid: canscan("TX", mp)}
last_time = -1.
line_number = 0
must_scan = False
data = bytearray()
for line in canfile:
line_number += 1
parts = line.split()
if len(parts) < 7:
if parts:
report("Ignoring line: %s" % (line.strip(),),
(line_number, 0.), is_err=True)
continue
p_ts = parts[0]
p_canid = parts[5]
p_len = parts[6]
p_data = parts[7:]
if (not p_ts.startswith('(') or not p_ts.endswith(')')
or not p_len.startswith('[') or not p_len.endswith(']')):
report("Ignoring line: %s" % (line.strip(),),
(line_number, 0.), is_err=True)
continue
new_time = float(p_ts[1:-1])
line_info = (line_number, new_time)
if new_time < last_time:
report("Backwards time %.6f vs %.6f: %s"
% (new_time, last_time, line.strip()),
line_info, is_err=True)
last_time = new_time
hdlr = handlers.get(p_canid)
if hdlr is not None:
newdata = [int(i, 16) for i in p_data]
hdlr.handle_data(line_info, line, newdata)
def main():
usage = "%prog <candump.log> <canid> <mcu.dict>"
opts = optparse.OptionParser(usage)
options, args = opts.parse_args()
if len(args) != 3:
opts.error("Incorrect number of arguments")
canfilename, canid, dictfilename = args
canid = int(canid, 16)
import_msgproto()
dictionary = read_dictionary(dictfilename)
canfile = open(canfilename, "r")
read_candump(canfile, canid, dictionary)
canfile.close()
if __name__ == '__main__':
main()

View File

@@ -44,6 +44,27 @@ BOARD_DEFS = {
"firmware_path": "Robin_e3.bin",
"current_firmware_path": "Robin_e3.cur"
},
'btt-octopus-f407-v1': {
'mcu': "stm32f407xx",
'spi_bus': "swspi",
'spi_pins': "PC8,PD2,PC12",
'cs_pin': "PC11",
'skip_verify': True
},
'btt-octopus-f429-v1': {
'mcu': "stm32f429xx",
'spi_bus': "swspi",
'spi_pins': "PC8,PD2,PC12",
'cs_pin': "PC11",
'skip_verify': True
},
'btt-octopus-f446-v1': {
'mcu': "stm32f446xx",
'spi_bus': "swspi",
'spi_pins': "PC8,PD2,PC12",
'cs_pin': "PC11",
'skip_verify': True
},
'btt-skr-pro': {
'mcu': "stm32f407xx",
'spi_bus': "swspi",
@@ -66,6 +87,27 @@ BOARD_DEFS = {
'spi_bus': 'spi3a',
'cs_pin': 'PA15'
},
'btt-skr-3-h743': {
'mcu': 'stm32h743xx',
'spi_bus': 'swspi',
'spi_pins': "PC8,PD2,PC12",
'cs_pin': 'PC11',
'skip_verify': True
},
'btt-skr-3-h723': {
'mcu': 'stm32h723xx',
'spi_bus': 'swspi',
'spi_pins': "PC8,PD2,PC12",
'cs_pin': 'PC11',
'skip_verify': True
},
'creality-v4.2.2': {
'mcu': "stm32f103xe",
'spi_bus': "swspi",
'spi_pins': "PC8,PD2,PC12",
'cs_pin': "PC11",
'skip_verify': True
},
'monster8': {
'mcu': "stm32f407xx",
'spi_bus': "spi3a",
@@ -75,6 +117,17 @@ BOARD_DEFS = {
'mcu': "stm32f405xx",
'spi_bus': "spi1",
"cs_pin": "PA4"
},
'fysetc-cheetah': {
'mcu': "stm32f401xc",
'spi_bus': "spi1",
"cs_pin": "PA4",
"current_firmware_path": "OLD.BIN"
},
'btt-skrat': {
'mcu': "stm32g0b1xx",
'spi_bus': "spi1",
"cs_pin": "PB8"
}
}
@@ -102,16 +155,28 @@ BOARD_ALIASES = {
'btt-skr-mini-mz': BOARD_DEFS['btt-skr-mini'],
'btt-skr-e3-dip': BOARD_DEFS['btt-skr-mini'],
'btt002-v1': BOARD_DEFS['btt-skr-mini'],
'creality-v4.2.7': BOARD_DEFS['btt-skr-mini'],
'creality-v4.2.7': BOARD_DEFS['creality-v4.2.2'],
'btt-skr-2-f407': BOARD_DEFS['btt-octopus-f407-v1'],
'btt-skr-2-f429': BOARD_DEFS['btt-octopus-f429-v1'],
'btt-octopus-f407-v1.0': BOARD_DEFS['btt-octopus-f407-v1'],
'btt-octopus-f407-v1.1': BOARD_DEFS['btt-octopus-f407-v1'],
'btt-octopus-f429-v1.0': BOARD_DEFS['btt-octopus-f429-v1'],
'btt-octopus-f429-v1.1': BOARD_DEFS['btt-octopus-f429-v1'],
'btt-octopus-f446-v1.0': BOARD_DEFS['btt-octopus-f446-v1'],
'btt-octopus-f446-v1.1': BOARD_DEFS['btt-octopus-f446-v1'],
'btt-octopus-pro-f429-v1.0': BOARD_DEFS['btt-octopus-f429-v1'],
'btt-octopus-pro-f446-v1.0': BOARD_DEFS['btt-octopus-f446-v1'],
'btt-octopus-pro-h723-v1.1': BOARD_DEFS['btt-skr-3-h723'],
'btt-skr-pro-v1.1': BOARD_DEFS['btt-skr-pro'],
'btt-skr-pro-v1.2': BOARD_DEFS['btt-skr-pro'],
'btt-gtr-v1': BOARD_DEFS['btt-gtr'],
'mks-robin-e3d': BOARD_DEFS['mks-robin-e3'],
'fysetc-cheetah-v2': BOARD_DEFS['fysetc-cheetah'],
'fysetc-spider-v1': BOARD_DEFS['fysetc-spider'],
'fysetc-s6-v1.2': BOARD_DEFS['fysetc-spider'],
'fysetc-s6-v2': BOARD_DEFS['fysetc-spider'],
'monster8': BOARD_DEFS['monster8'],
'robin_v3': BOARD_DEFS['monster8']
'robin_v3': BOARD_DEFS['monster8'],
'btt-skrat-v1.0': BOARD_DEFS['btt-skrat']
}
def list_boards():

View File

@@ -1,7 +1,8 @@
#!/usr/bin/env python2
# Module supporting uploads Klipper firmware to an SD Card via SPI
# Module supporting uploads Klipper firmware to an SD Card via SPI and SDIO
#
# Copyright (C) 2021 Eric Callahan <arksine.code@gmail.com>
# Copyright (C) 2022 H. Gregor Molter <gregor.molter@secretlab.de>
#
# This file may be distributed under the terms of the GNU GPLv3 license.
import sys
@@ -36,7 +37,7 @@ def output(msg):
sys.stdout.write("%s" % (msg,))
sys.stdout.flush()
def calc_crc7(data):
def calc_crc7(data, with_padding=True):
# G(x) = x^7 + x^3 + 1
# Shift left as we are only calculating a 7 bit CRC
poly = 0b10001001 << 1
@@ -47,6 +48,8 @@ def calc_crc7(data):
crc = (crc << 1) ^ poly if crc & 0x80 else crc << 1
# The sdcard protocol likes the crc left justfied with a
# padded bit
if not with_padding:
return crc
return crc | 1
def calc_crc16(data):
@@ -89,11 +92,12 @@ def check_need_convert(board_name, config):
###########################################################
#
# SPI FLash Implementation
# SPI / SDIO Flash Implementation
#
###########################################################
SPI_OID = 0
SDIO_OID = 0
SPI_MODE = 0
SD_SPI_SPEED = 400000
# MCU Command Constants
@@ -114,6 +118,20 @@ SW_SPI_BUS_CMD = "spi_set_software_bus oid=%d " \
SPI_SEND_CMD = "spi_send oid=%c data=%*s"
SPI_XFER_CMD = "spi_transfer oid=%c data=%*s"
SPI_XFER_RESPONSE = "spi_transfer_response oid=%c response=%*s"
SDIO_CFG_CMD = "config_sdio oid=%d blocksize=%u"
SDIO_BUS_CMD = "sdio_set_bus oid=%d sdio_bus=%s"
SDIO_SEND_CMD = "sdio_send_command oid=%c cmd=%c argument=%u wait=%c"
SDIO_SEND_CMD_RESPONSE = "sdio_send_command_response oid=%c error=%c " \
"response=%*s"
SDIO_READ_DATA="sdio_read_data oid=%c cmd=%c argument=%u"
SDIO_READ_DATA_RESPONSE="sdio_read_data_response oid=%c error=%c read=%u"
SDIO_WRITE_DATA="sdio_write_data oid=%c cmd=%c argument=%u"
SDIO_WRITE_DATA_RESPONSE="sdio_write_data_response oid=%c error=%c write=%u"
SDIO_READ_DATA_BUFFER="sdio_read_data_buffer oid=%c offset=%u len=%c"
SDIO_READ_DATA_BUFFER_RESPONSE="sdio_read_data_buffer_response oid=%c data=%*s"
SDIO_WRITE_DATA_BUFFER="sdio_write_data_buffer oid=%c offset=%u data=%*s"
SDIO_SET_SPEED="sdio_set_speed oid=%c speed=%u"
FINALIZE_CFG_CMD = "finalize_config crc=%d"
class SPIFlashError(Exception):
@@ -135,6 +153,40 @@ class SPIDirect:
def spi_transfer(self, data):
return self._spi_transfer_cmd.send([self.oid, data])
class SDIODirect:
def __init__(self, ser):
self.oid = SDIO_OID
self._sdio_send_cmd = mcu.CommandQueryWrapper(
ser, SDIO_SEND_CMD, SDIO_SEND_CMD_RESPONSE, self.oid)
self._sdio_read_data = mcu.CommandQueryWrapper(
ser, SDIO_READ_DATA, SDIO_READ_DATA_RESPONSE, self.oid)
self._sdio_write_data = mcu.CommandQueryWrapper(
ser, SDIO_WRITE_DATA, SDIO_WRITE_DATA_RESPONSE, self.oid)
self._sdio_read_data_buffer = mcu.CommandQueryWrapper(
ser, SDIO_READ_DATA_BUFFER, SDIO_READ_DATA_BUFFER_RESPONSE,
self.oid)
self._sdio_write_data_buffer = mcu.CommandWrapper(ser,
SDIO_WRITE_DATA_BUFFER)
self._sdio_set_speed = mcu.CommandWrapper(ser, SDIO_SET_SPEED)
def sdio_send_cmd(self, cmd, argument, wait):
return self._sdio_send_cmd.send([self.oid, cmd, argument, wait])
def sdio_read_data(self, cmd, argument):
return self._sdio_read_data.send([self.oid, cmd, argument])
def sdio_write_data(self, cmd, argument):
return self._sdio_write_data.send([self.oid, cmd, argument])
def sdio_read_data_buffer(self, offset, length=32):
return self._sdio_read_data_buffer.send([self.oid, offset, length])
def sdio_write_data_buffer(self, offset, data):
return self._sdio_write_data_buffer.send([self.oid, offset, data])
def sdio_set_speed(self, speed):
return self._sdio_set_speed.send([self.oid, speed])
# FatFs Constants. Enums are implemented as lists. The item's index is its value
DRESULT = ['RES_OK', 'RES_ERROR', 'RES_WRPRT', 'RES_NOTRDY', 'RES_PARERR']
@@ -154,8 +206,11 @@ SECTOR_SIZE = 512
# FAT16/32 File System Support
class FatFS:
def __init__(self, ser):
self.sdcard = SDCardSPI(ser)
def __init__(self, ser, spi=True):
if spi:
self.sdcard = SDCardSPI(ser)
else:
self.sdcard = SDCardSDIO(ser)
self.disk_status = STA_NO_INIT | STA_NO_DISK
self.ffi_callbacks = []
self.ffi_main, self.ffi_lib = fatfs_lib.get_fatfs_ffi()
@@ -429,6 +484,10 @@ class SDCardFile:
SD_COMMANDS = {
'GO_IDLE_STATE': 0,
'ALL_SEND_CID': 2,
'SET_REL_ADDR': 3,
'SET_BUS_WIDTH': 6,
'SEL_DESEL_CARD': 7,
'SEND_IF_COND': 8,
'SEND_CSD': 9,
'SEND_CID': 10,
@@ -785,6 +844,329 @@ class SDCardSPI:
if err_msgs:
raise OSError("\n".join(err_msgs))
class SDCardSDIO:
def __init__(self, ser):
self.sdio = SDIODirect(ser)
self.rca = 0
self.reactor = ser.get_reactor()
self.enable_crc = True
self.mutex = self.reactor.mutex()
self.initialized = False
self.sd_version = 0
self.high_capacity = False
self.write_protected = False
self.total_sectors = 0
self.card_info = collections.OrderedDict()
def init_sd(self):
def check_for_ocr_errors(reg):
# returns False if an error flag is set
return ((reg[0]&0xFD) | (reg[1]&0xFF) |
(reg[2]&0xE0) | (reg[3]&0x08)) == 0
with self.mutex:
if self.initialized:
return
# Send reset command (CMD0)
if not self._send_command('GO_IDLE_STATE', 0):
raise OSError(
"flash_sdcard: failed to reset SD Card\n"
"Note that older (Version 1.0) SD cards can not be\n"
"hot swapped. Execute FIRMWARE_RESTART with the card\n"
"inserted for successful initialization.")
# Check Voltage Range (CMD8). Only Cards meeting the v2.0 spec
# support this. V1.0 cards (and MMC) will return illegal command.
check_pattern = 0b1010
resp = self._send_command_with_response(
'SEND_IF_COND', (1 << 8) | check_pattern)
resp = resp.strip(b'\xFF')
if len(resp) != 4:
# CMD8 is illegal, this is a version 1.0 card
self.sd_version = 1
else:
self.sd_version = 2
if not (resp[-2] == 1 and resp[-1] == check_pattern):
raise OSError("flash_sdcard: SD Card not running in a "
"compatible voltage range")
if self.sd_version == 2:
# Init card and come out of idle (ACMD41)
# Version 2 Cards may init before checking the OCR
# Allow vor LVDS card with 1.8v, too.
resp = self._check_command(lambda x: x[0]>>7 == 1,
'SD_SEND_OP_COND', 0xC1100000, is_app_cmd=True,
ignoreCRC=True)
if resp is None:
raise OSError("flash_sdcard: SD Card did not come"
" out of IDLE after reset")
if len(resp) == 4:
if self.sd_version == 1:
# Check acceptable volatage range for V1 cards
if resp[1] != 0xFF:
raise OSError("flash_sdcard: card does not support"
" 3.3v range")
elif self.sd_version == 2:
# Determine if this is a high capacity sdcard
if resp[0] & 0x40:
self.high_capacity = True
else:
raise OSError("flash_sdcard: Invalid OCR Response")
if self.sd_version == 1:
# Init card and come out of idle (ACMD41)
# Version 1 Cards do this after checking the OCR
if not self._check_command(0, 'SD_SEND_OP_COND', 0,
is_app_cmd=True):
raise OSError("flash_sdcard: SD Card did not come"
" out of IDLE after reset")
# Read out CID information register
self._process_cid_reg()
# Get card's relative address (RCA)
resp = self._send_command_with_response('SET_REL_ADDR', 0)
# Check if bits 15:13 have some error set
if (resp[-2] & 0xe0) != 0:
raise OSError("flash_sdcard: set card's "
"relative address failed")
self.rca = resp[0]<<8 | resp[1]
# Read out CSD information register
self._process_csd_reg()
# Select the current card
if not self._check_command(check_for_ocr_errors, 'SEL_DESEL_CARD',
self.rca << 16, tries=1):
raise OSError("flash_sdcard: failed to select the card")
# Set SDIO clk speed to approx. 1 MHz
self.sdio.sdio_set_speed(1000000)
if self._check_command(check_for_ocr_errors, 'SET_BLOCKLEN',
SECTOR_SIZE, tries=5):
self.initialized = True
else:
raise OSError("flash_sdcard: failed to set block size")
def deinit(self):
with self.mutex:
if self.initialized:
# Reset the SD Card
try:
if not self._send_command('GO_IDLE_STATE', 0):
logging.info("flash_sdcard: failed to reset SD Card")
except Exception:
logging.exception("Error resetting SD Card")
self.initialized = False
self.sd_version = 0
self.high_capacity = False
self.total_sectors = 0
self.card_info.clear()
def _check_command(self, check_func, cmd, args, is_app_cmd=False, tries=15,
ignoreCRC=False):
func = self._send_app_cmd_with_response if is_app_cmd else \
self._send_command_with_response
while True:
resp, rt = func(cmd, args, get_rt=True, ignoreCRC=ignoreCRC)
#logging.info("flash_sdcard: Check cmd %s, response: %s"
# % (cmd, repr(resp)))
if resp and check_func(resp):
return resp
tries -= 1
if tries < 1:
return None
self.reactor.pause(rt + .1)
def _send_command(self, cmd, args, wait=0):
cmd_code = SD_COMMANDS[cmd]
argument = 0
if isinstance(args, int) or isinstance(args, long):
argument = args & 0xFFFFFFFF
elif isinstance(args, list) and len(args) == 4:
argument = ((args[0] << 24) & 0xFF000000) | \
((args[1] << 16) & 0x00FF0000) | \
((args[2] << 8) & 0x0000FF00) | \
((args[3] << 0) & 0x000000FF)
else:
raise OSError("flash_sdcard: Invalid SD Card Command argument")
params = self.sdio.sdio_send_cmd(cmd_code, argument, wait)
#logging.debug(f'_send_command({cmd=}, {args=}, {wait=}) -> '
# 'CMD: {cmd_code} ARG: {argument} -> {params=}')
if (wait == 0):
# Just return the error code if no response was requested
return params['error'] == 0
return params
def _send_command_with_response(self, cmd, args, check_error=True,
ignoreCRC=False, get_rt=False):
# Wait for a short response
params = self._send_command(cmd, args, wait=1)
response = params['response']
if check_error:
if params['error'] != 0:
if ignoreCRC and params['error'] != 4:
response = []
if get_rt:
return bytearray(response), params['#receive_time']
else:
return bytearray(response)
def _send_app_cmd_with_response(self, cmd, args,
ignoreCRC=False, get_rt=False):
# CMD55 tells the SD Card that the next command is an
# Application Specific Command.
self._send_command_with_response('APP_CMD', self.rca << 16)
return self._send_command_with_response(
cmd, args, ignoreCRC=ignoreCRC, get_rt=get_rt)
def _process_cid_reg(self):
params = self._send_command('ALL_SEND_CID', 0, wait=2)
reg = bytearray(params['response'])
if reg is None:
raise OSError("flash_sdcard: Error reading CID register")
cid = collections.OrderedDict()
cid['manufacturer_id'] = reg[0]
cid['oem_id'] = reg[1:3].decode(encoding='ascii', errors='ignore')
cid['product_name'] = reg[3:8].decode(
encoding='ascii', errors='ignore')
cid['product_revision'] = str(reg[8] >> 4 & 0xFF) + "." \
+ str(reg[8] & 0xFF)
cid['serial_number'] = "".join(["%02X" % (c,) for c in reg[9:13]])
mfg_year = (((reg[13] & 0xF) << 4) | ((reg[14] >> 4) & 0xF)) + 2000
mfg_month = reg[14] & 0xF
cid['manufacturing_date'] = "%d/%d" % (mfg_month, mfg_year)
crc = calc_crc7(reg[:15], with_padding=False)
if crc != reg[15]:
raise OSError("flash_sdcard: CID crc mismatch: 0x%02X, recd: 0x%02X"
% (crc, reg[15]))
self.card_info.update(cid)
def _process_csd_reg(self):
params = self._send_command('SEND_CSD', self.rca << 16, wait=2)
reg = bytearray(params['response'])
if reg is None:
raise OSError("flash_sdcard: Error reading CSD register")
str_capacity = "Invalid"
max_capacity = 0
csd_type = (reg[0] >> 6) & 0x3
if csd_type == 0:
# Standard Capacity (CSD Version 1.0)
max_block_len = 2**(reg[5] & 0xF)
c_size = ((reg[6] & 0x3) << 10) | (reg[7] << 2) | \
((reg[8] >> 6) & 0x3)
c_mult = 2**((((reg[9] & 0x3) << 1) | (reg[10] >> 7)) + 2)
max_capacity = (c_size + 1) * c_mult * max_block_len
str_capacity = "%.1f MiB" % (max_capacity / (1024.0**2))
elif csd_type == 1:
# High Capacity (CSD Version 2.0)
c_size = ((reg[7] & 0x3F) << 16) | (reg[8] << 8) | reg[9]
max_capacity = (c_size + 1) * 512 * 1024
str_capacity = "%.1f GiB" % (max_capacity / (1024.0**3))
else:
logging.info("sdcard: Unsupported csd type: %d" % (csd_type))
self.write_protected = (reg[14] & 0x30) != 0
crc = calc_crc7(reg[:15], with_padding=False)
if crc != reg[15]:
raise OSError("flash_sdcard: CSD crc mismatch: 0x%02X, recd: 0x%02X"
% (crc, reg[15]))
self.card_info['capacity'] = str_capacity
self.total_sectors = max_capacity // SECTOR_SIZE
def print_card_info(self, print_func=logging.info):
print_func("\nSD Card Information:")
print_func("Version: %.1f" % (self.sd_version))
print_func("SDHC/SDXC: %s" % (self.high_capacity))
print_func("Write Protected: %s" % (self.write_protected))
print_func("Sectors: %d" % (self.total_sectors,))
for name, val in self.card_info.items():
print_func("%s: %s" % (name, val))
def read_sector(self, sector):
buf = None
err_msg = "flash_sdcard: read error, sector %d" % (sector,)
with self.mutex:
if not 0 <= sector < self.total_sectors:
err_msg += " out of range"
elif not self.initialized:
err_msg += ", SD Card not initialized"
else:
offset = sector
if not self.high_capacity:
offset = sector * SECTOR_SIZE
params = self.sdio.sdio_read_data(
SD_COMMANDS['READ_SINGLE_BLOCK'], offset)
if params['error'] != 0:
raise OSError(
'Read data failed. Error code=%d' %(params['error'],) )
if params['read'] != SECTOR_SIZE:
raise OSError(
'Read data failed. Expected %d bytes but got %d.' %
(SECTOR_SIZE, params['read']) )
buf = bytearray()
offset = 0
while SECTOR_SIZE-len(buf)>0:
rest = min(SECTOR_SIZE-len(buf), 32)
params = self.sdio.sdio_read_data_buffer(
offset, length=rest)
temp = bytearray(params['data'])
if len(temp) == 0:
raise OSError("Read zero bytes from buffer")
buf += temp
offset += len(temp)
if buf is None:
raise OSError(err_msg)
return buf
def write_sector(self, sector, data):
with self.mutex:
if not 0 <= sector < self.total_sectors:
raise OSError(
"flash_sdcard: write error, sector number %d invalid"
% (sector))
if not self.initialized:
raise OSError("flash_sdcard: write error, SD Card not"
" initialized")
outbuf = bytearray(data)
if len(outbuf) > SECTOR_SIZE:
raise OSError("sd_card: Cannot write sector larger"
" than %d bytes"
% (SECTOR_SIZE))
elif len(outbuf) < SECTOR_SIZE:
outbuf += bytearray([0] * (SECTOR_SIZE - len(outbuf)))
offset = sector
if not self.high_capacity:
offset = sector * SECTOR_SIZE
CHUNKSIZE = 32
for i in range(0, SECTOR_SIZE, CHUNKSIZE):
self.sdio.sdio_write_data_buffer(i, outbuf[i:i+CHUNKSIZE])
params = self.sdio.sdio_write_data(
SD_COMMANDS['WRITE_BLOCK'], offset)
if (params['error'] != 0) or (params['write'] != SECTOR_SIZE):
raise OSError(
"flash_sdcard: Error writing to sector %d"% (sector,))
status = self._send_command_with_response(
'SEND_STATUS', self.rca << 16)
if len(status) != 4:
raise OSError("flash_sdcard: Failed to get status response"
" after write: %s" % (repr(status),))
if ((status[3]>>1) & 0x0F) != 0:
# Bit 12:9 are not "0" (card is in idle)
raise OSError("flash_sdcard: Write error."
" Card is not in transfer state: 0x%02X"
% (((status[3]>>1) & 0x0F)))
SDIO_WARNING = """
This board requires a manual reboot to complete the flash process.
If the board's bootloader uses SDIO mode for its SDCard, then a full
power cycle is required. Please perform the power cycle now and then
rerun this utility with the 'check' option to verify flash.
"""
class MCUConnection:
def __init__(self, k_reactor, device, baud, board_cfg):
self.reactor = k_reactor
@@ -889,7 +1271,7 @@ class MCUConnection:
return True
return False
def configure_mcu(self, printfunc=logging.info):
def _configure_mcu_spibus(self, printfunc=logging.info):
# TODO: add commands for buttons? Or perhaps an endstop? We
# just need to be able to query the status of the detect pin
cs_pin = self.board_config['cs_pin'].upper()
@@ -945,6 +1327,41 @@ class MCUConnection:
raise SPIFlashError(
"Failed to Initialize SD Card. Is it inserted?")
def _configure_mcu_sdiobus(self, printfunc=logging.info):
bus = self.board_config['sdio_bus']
bus_enums = self.enumerations.get(
'sdio_bus', self.enumerations.get('bus'))
pin_enums = self.enumerations.get('pin')
if bus not in bus_enums:
raise SPIFlashError("Invalid SDIO Bus: %s" % (bus,))
bus_cmd = SDIO_BUS_CMD % (SDIO_OID, bus)
sdio_cfg_cmd = SDIO_CFG_CMD % (SDIO_OID, SECTOR_SIZE)
cfg_cmds = [ALLOC_OIDS_CMD % (1,), sdio_cfg_cmd, bus_cmd]
for cmd in cfg_cmds:
self._serial.send(cmd)
config_crc = zlib.crc32('\n'.join(cfg_cmds).encode()) & 0xffffffff
self._serial.send(FINALIZE_CFG_CMD % (config_crc,))
config = self.get_mcu_config()
if not config["is_config"] or config["is_shutdown"]:
raise MCUConfigError("Failed to configure MCU")
printfunc("Initializing SD Card and Mounting file system...")
self.fatfs = FatFS(self._serial,spi=False)
self.reactor.pause(self.reactor.monotonic() + .5)
try:
self.fatfs.mount(printfunc)
except OSError:
logging.exception("SD Card Mount Failure")
raise SPIFlashError(
"Failed to Initialize SD Card. Is it inserted?")
def configure_mcu(self, printfunc=logging.info):
if 'spi_bus' in self.board_config:
self._configure_mcu_spibus(printfunc=printfunc)
elif 'sdio_bus' in self.board_config:
self._configure_mcu_sdiobus(printfunc=printfunc)
else:
raise SPIFlashError("Unknown bus defined in board_defs.py.")
def sdcard_upload(self):
output("Uploading Klipper Firmware to SD Card...")
input_sha = hashlib.sha1()
@@ -989,6 +1406,9 @@ class MCUConnection:
return sd_chksm
def verify_flash(self, req_chksm, old_dictionary, req_dictionary):
if bool(self.board_config.get('skip_verify', False)):
output_line(SDIO_WARNING)
return
output("Verifying Flash...")
validation_passed = False
msgparser = self._serial.get_msgparser()
@@ -1063,6 +1483,7 @@ class SPIFlash:
self.firmware_checksum = None
self.task_complete = False
self.need_upload = True
self.need_verify = True
self.old_dictionary = None
self.new_dictionary = None
if args['klipper_dict_path'] is not None:
@@ -1092,7 +1513,7 @@ class SPIFlash:
raise SPIFlashError("Unable to reconnect")
output_line("Done")
def run_reset(self, eventtime):
def run_reset_upload(self, eventtime):
# Reset MCU to default state if necessary
self.mcu_conn.connect()
if self.mcu_conn.check_need_restart():
@@ -1102,6 +1523,16 @@ class SPIFlash:
self.need_upload = False
self.run_sdcard_upload(eventtime)
def run_reset_verify(self, eventtime):
# Reset MCU to default state if necessary
self.mcu_conn.connect()
if self.mcu_conn.check_need_restart():
self.mcu_conn.reset()
self.task_complete = True
else:
self.need_verify = False
self.run_verify(eventtime)
def run_sdcard_upload(self, eventtime):
# Reconnect and upload
if not self.mcu_conn.connected:
@@ -1121,7 +1552,8 @@ class SPIFlash:
def run_verify(self, eventtime):
# Reconnect and verify
self.mcu_conn.connect()
if not self.mcu_conn.connected:
self.mcu_conn.connect()
self.mcu_conn.configure_mcu()
self.mcu_conn.verify_flash(self.firmware_checksum, self.old_dictionary,
self.new_dictionary)
@@ -1148,12 +1580,18 @@ class SPIFlash:
self.mcu_conn = k_reactor = None
def run(self):
self.run_reactor_task(self.run_reset)
self._wait_for_reconnect()
if self.need_upload:
self.run_reactor_task(self.run_sdcard_upload)
if not bool(self.board_config.get('verify_only', False)):
self.run_reactor_task(self.run_reset_upload)
self._wait_for_reconnect()
self.run_reactor_task(self.run_verify)
if self.need_upload:
self.run_reactor_task(self.run_sdcard_upload)
self._wait_for_reconnect()
self.run_reactor_task(self.run_verify)
else:
self.run_reactor_task(self.run_reset_verify)
if self.need_verify:
self._wait_for_reconnect()
self.run_reactor_task(self.run_verify)
def main():
parser = argparse.ArgumentParser(
@@ -1177,6 +1615,9 @@ def main():
parser.add_argument(
"-d", "--dict_path", metavar="<klipper.dict>", type=str,
default=None, help="Klipper firmware dictionary")
parser.add_argument(
"-c","--check", action="store_true",
help="Perform flash check/verify only")
parser.add_argument(
"device", metavar="<device>", help="Device Serial Port")
parser.add_argument(
@@ -1195,6 +1636,10 @@ def main():
flash_args['baud'] = args.baud
flash_args['klipper_bin_path'] = args.klipper_bin_path
flash_args['klipper_dict_path'] = args.dict_path
flash_args['verify_only'] = args.check
if args.check:
# override board_defs setting when doing verify-only:
flash_args['skip_verify'] = False
check_need_convert(args.board, flash_args)
fatfs_lib.check_fatfs_build(output)
try:

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# Encodes STM32 firmwares to be flashable from SD card by Chitu motherboards.
# Relocate firmware to 0x08008800!

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2
#!/usr/bin/env python3
# Script to update firmware for MKS Robin bootloader
#
# Copyright (C) 2020 Kevin O'Connor <kevin@koconnor.net>