Skip to content
IRSPlaysPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

VectorM

Bare-metal firmware for a 6-DOF robot arm: precise, glitch-free servo motion over a CRC-checked serial protocol.

STM32F103C8T6 · C (no HAL, no RTOS) · 6.7 KB flash · Python host library · built for the arm in this AliExpress listing

VectorM replaces the stock firmware on the arm's STM32 controller board. The PC does the thinking (inverse kinematics, planning, vision) and streams joint angles over serial; the STM32 turns them into hardware-timed PWM pulses for the six servos and keeps them on a 1 kHz motion engine, so the arm settles on target without overshoot or jitter.

It is the body of Cyph, the Furry Engineer, a pixel-art AI companion that talks, plans your day and drives this arm. VectorM works on its own too: anything that can speak the protocol below can move the arm.

Hardware. I built this for the arm kit from the AliExpress listing (the WIT open-source vision robotic arm: STM32F103C8T6 controller, six 20 kg·cm 270° PWM servos, an OpenMV camera). Listings change and boards get revised, so if yours differs, check the pin map in firmware/inc/config.h first (see If your board differs).

Why it is precise

Item Value
PWM timer resolution 2 MHz tick = 0.5 µs per step
Angular resolution 0.068° per step (the servos are good to about 0.3°)
Pulse period 20.000 ms exactly (50.00 Hz), hardware-timed
Motion update rate 1 kHz, smootherstep (5th-order minimum-jerk) profile
Pulse updates timer-preloaded: changes latch at the cycle boundary, zero glitches
Angle math integer centidegrees (0.01°), no float drift
Framing CRC-16/CCITT, sequence numbers, ACK/NAK on every command
Calibration per-servo min/max pulse, trim and direction, saved to flash
Size 6,708 bytes of the 64 KB flash

Point-to-point moves use a minimum-jerk profile (zero velocity and acceleration at both ends), so there is no overshoot and no vibration. Re-targeting mid-move always starts from the live interpolated position, never a jump. Streaming mode plays pre-computed waypoint trajectories with sample-exact timing from a 32-point queue.

Quick start

You need the arm, an 8.4 V / 3 A supply for the servos, and a USB-serial link to the board's Serial Comm Port (a USB-TTL adapter, an Arduino Uno used as a bridge, or the OpenMV camera as a bridge: all three are covered below).

pip install pyserial
git clone https://github.com/IRSPlays/VectorM.git
cd VectorM/host

# flash the prebuilt firmware (BOOT0 = 1, press RESET first; then BOOT0 = 0 and RESET after)
python flash_stm32.py --port COM15 --bin ../firmware/build/vectorm_arm.bin

# move the arm
python -c "from arm_client import ArmClient; a=ArmClient('COM15'); print(a.ping()); a.move_joints([135]*6, time_ms=1500)"

flash_stm32.py talks to the chip's ROM bootloader at 8E1, erases only the pages it needs, writes, reads everything back to verify, jumps to the app and PINGs it. A real run looks like this (from flash_stm32_full.log while bringing the arm up):

bootloader v3.0, chip PID 0x0410
erasing pages 0..6...   erase done in 0.13s
writing 6708 bytes...   written in 2.19s
verifying (read-back compare)...  verify OK: 6708 bytes match the .bin exactly
application answered PING: {'version': '1.0.0', 'servos': 6, 'max_payload': 64, 'control_period_ms': 1}

Repository layout

firmware/
  inc/config.h         ALL tunables: pins, servo map, pulse ranges, calibration address
  inc/stm32f103.h      minimal register definitions (no vendor HAL)
  src/startup.c        vector table, 72 MHz clock init
  src/servo.c          PWM driver, angle <-> pulse, calibration in flash
  src/motion.c         1 kHz interpolation engine and stream queue
  src/protocol.c       binary framing, CRC, command dispatch
  src/uart.c           interrupt-driven ring buffers
  src/main.c
  build.py / Makefile  Windows / Linux, macOS, MSYS2
  linker.ld
  build/vectorm_arm.bin, .hex   the prebuilt firmware (byte-identical to what is flashed on my arm)
host/
  arm_client.py        Python control class (ArmClient)
  expressions.py       26 animal-like moods for the arm (Cipher mode)
  example_ik_control.py  where your inverse kinematics plugs in
  flash_stm32.py       serial-bootloader flasher with read-back verify
  pin_scan.py, pin_probe.py   find which pin drives which servo header (bring-up)
  openmv_serial_bridge.py     turn the OpenMV camera into a USB-serial bridge
tests/
  test_host_client.py  host library against a fake serial link
  test_protocol.c      real protocol.c + motion.c with stubbed servo/uart
  test_servo_map.c     angle <-> pulse mapping

Hardware

  • MCU: STM32F103C8T6, 72 MHz, 64 KB flash, 20 KB RAM
  • 6× PWM servos, 20 kg·cm, 5–8.4 V, 0.16 s per 60° at 7.4 V, 0.3° accuracy, 270° travel
  • Servo IDs from the base up: 1 base yaw · 2 shoulder · 3 elbow · 4 wrist pitch · 5 wrist roll · 6 gripper
  • Power: 8.4 V / 2 A minimum (3 A+ recommended) into the board's power terminal. Servos draw up to 2.5 A each at stall, so a brownout during a fast move is a supply problem, not a firmware problem.
  • Control link: the board's Serial Comm Port (TX / RX / GND / 5V) to a USB-serial adapter. The board's own USB connector is not the control channel (it goes to the F103's native USB peripheral).

Wiring

Servo signal pins (measured on the real board with host/pin_probe.py; the map lives in inc/config.h):

Servo Signal pin Timer
1 base yaw PB1 TIM3_CH4
2 shoulder PB0 TIM3_CH3
3 elbow PA7 TIM3_CH2
4 wrist pitch PA6 TIM3_CH1
5 wrist roll PB6 TIM4_CH1
6 gripper PB7 TIM4_CH2

(PA8 also moved something during the scan and is unused.)

Serial link, USART1 at 115200 baud:

Board Serial Comm Port USB-TTL adapter
TX RX
RX TX
GND GND
5V only if the adapter should power the logic

A common ground between the adapter, the controller board and the servo supply is mandatory.

No USB-TTL adapter? Two bridges that work

An Arduino Uno R3. Make it a dumb bridge by pulling out the ATmega328P (or jumpering RESET to GND), then wire straight through: Uno pin 0 (RX) to STM32 RX (PA10), Uno pin 1 (TX) to STM32 TX (PA9), GND to GND. Do not connect 5 V. PA9/PA10 are 5 V tolerant, so no level shifter is needed.

The OpenMV camera already on the arm. Copy host/openmv_serial_bridge.py onto it and it becomes a USB-serial bridge to the STM32 (OpenMV P5/RX to PA9, P4/TX to PA10, GND to GND), usable for flashing and for control.

Building from source

Needs the Arm GNU toolchain (arm-none-eabi-gcc); the xPack build installs with npm i -g @xpack-dev-tools/arm-none-eabi-gcc.

cd firmware
python build.py        # Windows
make                   # Linux, macOS, MSYS2

Output: build/vectorm_arm.bin, .hex, .elf, .map. I rebuilt it for this repo and the .bin is byte-identical to the one flashed on the arm (6,708 bytes).

Flashing alternatives: an ST-Link (SWDIO/SWCLK/GND) with STM32CubeProgrammer at 0x08000000, or the serial bootloader with STM32CubeProgrammer (UART) or stm32flash.

Controlling it from Python

from arm_client import ArmClient

with ArmClient("COM15") as arm:
    arm.ping()
    arm.move_joints([135, 135, 135, 135, 135, 135], time_ms=1500)
    joints = your_ik_solver(x, y, z, gripper_deg)    # <- your math
    arm.move_joints(joints, time_ms=800)
    arm.stream_trajectory(waypoints, dt_ms=20)       # path playback
    arm.torque(False)                                # go limp to pose it by hand

Angles are degrees, joints 1..6 as above. The rest of the client: status(), move_joint(), move_raw() (calibration only), stop(), reboot(), and set_calibration() / get_calibration() / save_calibration() / reset_calibration().

Moods. host/expressions.py makes the arm act like a silly animal (this is what Cyph uses): 26 expressions, blended smoothly, with shoulder and elbow kept small so the arm cannot slam the table.

python expressions.py COM15 list                 # happy, excited, curious, sniff, sleepy, sad, startle, purr, zoomies, ...
python expressions.py COM15 happy excited sad    # chained
python expressions.py COM15 idle                 # endless random moods, Ctrl+C to stop

Protocol

Binary, little-endian:

AA 55 | LEN | SEQ | CMD | PAYLOAD... | CRC16_LO CRC16_HI
  • LEN = byte count of SEQ + CMD + PAYLOAD (2..66)
  • CRC-16/CCITT-FALSE over LEN, SEQ, CMD, PAYLOAD
  • Response: same framing, CMD | 0x80, SEQ echoed, first payload byte is the status: 0 OK, 1 BAD_CMD, 2 BAD_ARGS, 3 FULL, 4 FLASH_ERR
CMD Name Request payload Response payload (after status)
01 PING none fw ×3, servos, max_payload u16, period_ms u16
02 GET_STATUS none uptime u32, flags u8, stream_free u16, 6×(pos s32, target s32) in 0.01°
10 MOVE 6× s32 angle (0.01°), u16 time_ms, u8 profile (0 smooth, 1 linear) none
11 MOVE_RAW 6× u16 pulse_us (immediate, calibration only) none
12 STREAM 6× s32 angle (0.01°), u16 dt_ms u16 free queue slots
13 STOP none none
14 TORQUE u8 enable (0 = limp) none
20 SET_CALIB u8 id, u16 min_us, u16 max_us, s32 min (0.01°), s32 max (0.01°), s16 trim (0.01°), s8 dir none
21 GET_CALIB u8 id same struct as SET_CALIB
22 SAVE_CALIB none (4 = flash error)
23 DEFAULT_CALIB none erases stored calibration
30 REBOOT none none
40 PIN_PROBE u8 port (0 = A), u8 pin sweeps a servo signal on that GPIO for ~1.6 s (bring-up)

Calibration

Mechanical centering varies from servo to servo.

  1. move_raw([1500]*6) parks every servo at mid-pulse.
  2. Measure each joint's true angle and set trim_deg so commanded angle equals actual angle (set_calibration(id, trim_deg=...)).
  3. If a joint is wired mirrored, use direction=-1.
  4. Narrow min_us / max_us to the joint's real mechanical range, so clamping happens in software (important for the gripper).
  5. save_calibration() stores it in flash (at 0x0800FC00); it survives reboots.

If your board differs

Servo header order differs between board revisions. Find your map instead of guessing: run python host/pin_scan.py COM15, watch which joint moves for each pin and type its number (or Enter if nothing moved), then put the result in SERVO_PIN_MAP_INIT in firmware/inc/config.h and rebuild. pin_probe.py does the same for one pin you name.

Tests

python tests/test_host_client.py

This runs the host library against a fake serial link (framing, CRC, encoding, round trips) and passes. The two C tests (test_protocol.c runs the real protocol.c and motion.c with stubbed servo and UART layers; test_servo_map.c checks the angle-to-pulse mapping) need a native gcc (they compile the real firmware sources against stubs); I have not run them on this machine, which has only the ARM toolchain.

Honest limits: the tests never touch real servos, so they say nothing about your supply, wiring or calibration.

Troubleshooting

  • "USB device not recognized" when plugging the board's USB in. That USB is not the serial link. Use a USB-TTL adapter (or a bridge above) on the Serial Comm Port. If a C-to-C cable fails to enumerate, try USB-A to C: many boards lack the CC pull-downs.
  • Servos jitter on heavy moves. Servo supply sag. Use a stronger 8.4 V supply, thick power wires, and a common ground.
  • The arm twitches on boot. The firmware parks all servos at 135° on start-up to hold position; this is intentional. Use torque(False) to make the arm limp before posing it by hand.
  • The wrong joint moves. Your servo header order differs; see If your board differs.
  • Flashing fails. The bootloader needs even parity (8E1), BOOT0 = 1 and a RESET before you start; set BOOT0 = 0 and RESET afterwards.

Status

Working on my arm: flashing, motion, streaming, calibration and the 26 moods. Not done: closed-loop feedback (the servos have no position sensor, so the position read back is the commanded one), a watchdog that goes limp when the PC goes quiet (CMD_TIMEOUT_MS is 0 in config.h, so it is off), and a photo or video of the arm in this README.

License

No license has been chosen yet, so by default all rights are reserved. If you want to reuse the firmware, open an issue.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages