Skip to content

Instantly share code, notes, and snippets.

@instance-id
Created August 28, 2025 17:25
Show Gist options
  • Save instance-id/bdf16a4d3bcdbc292dedbaee62f7385b to your computer and use it in GitHub Desktop.
Save instance-id/bdf16a4d3bcdbc292dedbaee62f7385b to your computer and use it in GitHub Desktop.
Unlock and flash candleLight firmware to WeAct USB2CANFDV1 device via ST-Link v2
#!/usr/bin/env bash
#---------------------------------------------------------------------------------------------------------------
# Flash candleLight firmware onto the WeAct USB2CANFDV1 (https://github.com/WeActStudio/WeActStudio.USB2CANFDV1)
# auto-handling leftover write-protection / PCROP and (optionally) lowering RDP.
#---------------------------------------------------------------------------------------------------------------
# This script assumes you are using ST-Link V2 and it is connected to the device.
# Device used when creating/testing (https://www.aliexpress.us/item/3256805117494436.html - STLink-STM32 variant)
# This script came about from the following firmware port
# https://github.com/WeActStudio/WeActStudio.USB2CANFDV1/issues/4
# Follow steps in above issue post to build WeActStudio_USB2CANFDV1_fw
# Firmware location:
# https://github.com/romainreignier/candleLight_fw/tree/add_weactstudio_usb2canfdv1
# Once you have WeActStudio_USB2CANFDV1_fw.bin file:
# run this script to erase the device and flash new firmware.
# Usage:
# ./firmware_flash.sh # normal (will flash the specified firmware)
# ./firmware_flash.sh --unlock # clears WRP/PCROP if still set (requires RDP=0xAA)
# ./firmware_flash.sh --force-unlock # skip prompt when clearing protections
# ./firmware_flash.sh --auto-rdp # if RDP != 0xAA, lower it to 0xAA (TRIGGERS MASS ERASE)
# ./firmware_flash.sh --auto-rdp --unlock # perform full sequence automatically (ideal for new unmodified device)
#
# Notes:
# * Lowering RDP (Level 1 -> 0xAA) causes an automatic full chip erase by the MCU.
# * After lowering RDP, a separate mass erase is redundant; script detects this.
# ENV overrides: FIRMWARE_BUILD, FIRMWARE_NAME, CLI (STM32_Programmer_CLI path)
# github.com/instance-id
set -euo pipefail
CLI=${CLI:-STM32_Programmer_CLI}
FIRMWARE_BUILD=${FIRMWARE_BUILD:-/mnt/x/GitHub/romainreignier/candleLight_fw/build}
FIRMWARE_NAME=${FIRMWARE_NAME:-WeActStudio_USB2CANFDV1_fw.bin}
FIRMWARE_PATH="${FIRMWARE_BUILD}/${FIRMWARE_NAME}"
ADDR=0x08000000
DO_UNLOCK=0
FORCE=0
AUTO_RDP=0
RDP_LOWERED=0
while [[ $# -gt 0 ]]; do
case "$1" in
--unlock) DO_UNLOCK=1; shift;;
--force-unlock) DO_UNLOCK=1; FORCE=1; shift;;
--auto-rdp) AUTO_RDP=1; shift;;
-h|--help)
grep '^#' "$0" | sed 's/^# //;s/^#$//' ; exit 0 ;;
*) echo "Unknown arg: $1" >&2; exit 1;;
esac
done
if [[ ! -f "$FIRMWARE_PATH" ]]; then
echo "ERROR: Firmware file not found: $FIRMWARE_PATH" >&2
exit 1
fi
echo "Firmware: $FIRMWARE_PATH"
echo "Reading option bytes..."
OB_DUMP="$($CLI -c port=SWD -ob displ 2>/dev/null || true)"
extract_field() {
# Grep field like WRP1A_STRT and pull hex value after ':'
printf '%s\n' "$OB_DUMP" | awk -v k="$1" '$1==k{for(i=1;i<=NF;i++){if($i==":") {print $(i+1); exit}}}'
}
RDP_VAL=$(printf '%s\n' "$OB_DUMP" | grep -E '^[[:space:]]*RDP[[:space:]]*:' | grep -o '0x[0-9A-Fa-f]*' | head -n1 || true)
WRP1A_STRT=$(printf '%s' "$OB_DUMP" | grep -E 'WRP1A_STRT' | grep -o '0x[0-9A-Fa-f]+' | head -n1 || true)
WRP1A_END=$(printf '%s' "$OB_DUMP" | grep -E 'WRP1A_END' | grep -o '0x[0-9A-Fa-f]+' | head -n1 || true)
PCROP1A_STRT=$(printf '%s' "$OB_DUMP" | grep -E 'PCROP1A_STRT' | grep -o '0x[0-9A-Fa-f]+' | head -n1 || true)
PCROP1A_END=$(printf '%s' "$OB_DUMP" | grep -E 'PCROP1A_END ' | grep -o '0x[0-9A-Fa-f]+' | head -n1 || true)
echo "RDP=$RDP_VAL WRP1A_STRT=$WRP1A_STRT WRP1A_END=$WRP1A_END PCROP1A_STRT=$PCROP1A_STRT PCROP1A_END=$PCROP1A_END"
# --- Handle RDP lowering if requested ---
if [[ "$RDP_VAL" != "0xAA" ]]; then
echo "Device RDP is not 0xAA (current: $RDP_VAL). Flash/erase operations will be blocked or limited." >&2
if (( AUTO_RDP )); then
if (( ! FORCE )); then
read -r -p "Lower RDP to 0xAA now (WILL MASS ERASE)? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "Aborting per user choice"; exit 10; }
fi
echo "Lowering RDP to 0xAA (this triggers automatic mass erase)..."
$CLI -c port=SWD -ob RDP=0xAA || { echo "Failed to lower RDP" >&2; exit 11; }
RDP_LOWERED=1
echo "Re-reading option bytes after RDP change..."
OB_DUMP="$($CLI -c port=SWD -ob displ 2>/dev/null || true)"
RDP_VAL=$(printf '%s\n' "$OB_DUMP" | grep -E '^[[:space:]]*RDP[[:space:]]*:' | grep -o '0x[0-9A-Fa-f]*' | head -n1 || true)
echo "New RDP=$RDP_VAL"
if [[ "$RDP_VAL" != "0xAA" ]]; then
echo "ERROR: RDP did not change to 0xAA; aborting." >&2
exit 12
fi
# After RDP lowering, other protections may still exist; continue normal path.
else
echo "RDP not lowered. Re-run with --auto-rdp (optionally --force-unlock) or manually:"
echo " $CLI -c port=SWD -ob RDP=0xAA"
exit 9
fi
fi
NEEDS_UNLOCK=0
if [[ "$WRP1A_STRT" == "0x0" && "$WRP1A_END" == "0x1" ]]; then
NEEDS_UNLOCK=1
fi
if [[ "$PCROP1A_STRT" != "0x1FF" || "$PCROP1A_END" != "0x0" ]]; then
NEEDS_UNLOCK=1
fi
if (( NEEDS_UNLOCK )); then
echo "Detected active write/PCROP protection over initial flash pages."
if (( DO_UNLOCK )); then
if (( ! FORCE )); then
read -r -p "Proceed to clear WRP1A/PCROP1A (destructive if data remained)? [y/N] " ans
[[ "$ans" =~ ^[Yy]$ ]] || { echo "Aborting per user choice"; exit 2; }
fi
echo "Clearing WRP1A + PCROP1A..."
# Disable by setting to empty ranges (start high, end low pattern used by vendor)
$CLI -c port=SWD -ob WRP1A_STRT=0x7F WRP1A_END=0x0 PCROP1A_STRT=0x1FF PCROP1A_END=0x0 || {
echo "Failed to clear protections" >&2; exit 1; }
echo "Re-reading option bytes..."
$CLI -c port=SWD -ob displ | grep -E 'WRP1A_|PCROP1A_' || true
else
echo "Protection present. Re-run with --unlock (and optionally --force-unlock) to clear, then flash." >&2
exit 3
fi
else
echo "No blocking protection detected."
fi
if (( RDP_LOWERED )); then
echo "Mass erase already performed implicitly by RDP lowering. Skipping explicit mass erase."
else
echo "Performing mass erase (safest before flashing replacement firmware)..."
$CLI -c port=SWD -e all || { echo "Mass erase failed" >&2; exit 1; }
fi
echo "Flashing firmware..."
$CLI -c port=SWD -w "$FIRMWARE_PATH" $ADDR -v -rst || { echo "Flash command failed" >&2; exit 1; }
echo "Verifying a short readback of vector table..."
$CLI -c port=SWD -r8 $ADDR 0x100 /tmp/_vt_check.bin || echo "Readback check failed (non-fatal if protection re-enabled)."
hexdump -C /tmp/_vt_check.bin | head -n5 || true
echo "Done. Device should enumerate under candleLight / gs_usb now."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment