Feetech STS Driver
Loading...
Searching...
No Matches
Feetech STS Servo Driver (STM32)

A portable, dependency-free C11 core for Feetech STS series smart servos over half-duplex UART. The project includes an STM32F103 hardware port using DMA-driven half-duplex UART with IDLE-line reception. The core is covered by 198 host unit tests in CI. The current AF open-drain port has completed 304,227 live transactions with zero retries or hard communication failures on the tested single-servo bench, separately from 390,385 historical transactions.

CI - Feetech Driver Latest version

Quick links: API documentation | Hardware validation | Design decisions | Porting guide | Engineering postmortem


Status

  • Protocol and service core: implemented and covered by 198 passing host unit tests using Unity and CTest, gated in CI alongside a Cppcheck static-analysis pass.
  • STM32F103 port: validated across four historical on-target campaigns totalling 390,385 transactions with zero retries and zero hard failures. Full per-campaign figures and methodology are in docs/hardware-validation.md.
  • Port simplification: PA2 now stays in AF open-drain mode, removing per-packet GPIO switching and the unused scope marker.
  • Latest bench report: two complete AF_OD campaigns passed 400 runs and 12,800 individual tests, recording 304,227 transactions with zero retries or hard communication failures. The tested configuration uses 1.5 kOhm from PA2/DATA to 3.3 V at 1 Mbaud, 8N1; removing the resistor caused communication failure. Direction-separated captures measured approximately 195 ns command rise time and 5 ns reply rise time. See hardware validation.
  • In progress: Phase 1 electrical evidence closure (settled-low voltage and receiver-threshold margin), Phase 2 receive-state hardening, and Phase 3 cleanup under #10. Full-duplex adapter re-validation remains in #9. Current receive-timing limitations are documented in the porting guide.

An intermittent test failure was traced to state persisting between test runs rather than the initially suspected bus EMI. The investigation and supporting campaign logs are documented as an engineering postmortem in issue #8. The full hardware bring-up was merged in PR #11.


Features

  • Noise-resilient parsing: a sliding-window parser resynchronises after malformed data, false headers, and partial packet fragments
  • Zero heap allocation: all API buffers are caller-provided, making memory ownership explicit and keeping the core usable on targets without a heap
  • HAL-agnostic core: three injected function pointers decouple servo logic from the MCU UART implementation
  • Instrumented bus: per-transaction counters for transactions, retries, retry-saves, and hard failures support regression testing, hardware validation, and fault analysis
  • Granular error reporting: separate result codes distinguish validation, protocol, transport, timeout, and device faults
  • Portable: written in C11 with fixed-width types; no platform-specific dependencies in the core
  • Broad command coverage: position, speed, acceleration, PWM, step, torque, telemetry, EEPROM, and ID control
  • 198 host unit tests (Unity/CTest), plus an on-target hardware integration and stress suite

Architecture

Three layers, each independently testable:

Protocol layer (sts_protocol): stateless packet framing, checksum, and response parsing. It holds no knowledge of hardware or servo state, and each parse call has no shared parser state. Synchronisation of shared bus access remains the responsibility of the service or application layer.

Service layer (sts_servo, sts_servo_cmd): HAL-agnostic servo management built on the protocol layer. Handles bus wiring, servo handles, and all transactions through a single command engine (sts_execute_command), which also carries the transaction, retry, and failure counters. Platform UART is injected as function pointers via sts_bus_t, so the core has no MCU dependency. The command set (sts_servo_cmd) provides motion control, telemetry reads, and configuration built on the register-access primitives.

Port layer (Ports/sts_ports_stm32.c): the STM32F103 implementation of the injected transmit, receive, and flush contract: DMA transfers with IDLE-line variable-length reception, direct-wired half-duplex turnaround, and bounded error recovery. Implementing the transport callbacks ports the core to another MCU, with this file as a worked reference; see the porting guide.


Hardware Validation

The STM32F103 port is exercised by an on-target integration suite (Hardware_Tests/) over SEGGER RTT, covering protocol validation, position, speed and acceleration control, the torque state machine, and moving-status semantics. An instrumented stress runner drives repeated campaigns and reports pass, skip and fail counts, a per-test failure histogram, quiescence-gate activation, and bus-counter deltas.

Campaign Runs Transactions Retries Hard failures
Baseline 100 67,801 0 0
Intermediate 30 19,269 0 0
Final 200 152,980 0 0
Flush removal 200 150,335 0 0
Total 530 390,385 0 0

These historical figures cover the earlier GPIO-switching port, with the timed turnaround flush removed for the fourth campaign. The current AF_OD results are kept separate because they describe a different electrical configuration:

AF_OD campaign Runs passed Individual tests passed Transactions Retries Hard failures
Rise-time capture 200/200 6,400/6,400 152,140 0 0
Fall-time capture 200/200 6,400/6,400 152,087 0 0
Total 400/400 12,800/12,800 304,227 0 0

The Phase 1 report records a passed functional non-regression gate. Electrical evidence closure and subsequent software phases remain pending.

These figures characterise one bench configuration: a single MCU, servo, cable, and environment. They are direct observations, not a general reliability claim for the design. Methodology, per-campaign conditions, and the oscilloscope work that retracted an earlier transient hypothesis are documented in docs/hardware-validation.md.


Selected Design Decisions

Why are hardware errors on ping treated as online? STS_ERR_HARDWARE means the servo responded, so communication succeeded. The fault lies in the servo's internal state, such as overtemperature or overload, rather than the bus. Marking the servo offline in this case would be incorrect. Hardware error semantics belong in a higher application layer that has the context to make recovery decisions.

Why a centralised command engine? All service layer transactions route through a single sts_execute_command function. This keeps TX framing, RX receive, and response parsing in one place, gives retry policy and the transaction counters a single home, and provides one point of change for future work such as mutex protection or asynchronous IO.

Why caller-provided buffers? Dynamic allocation can introduce variable latency, fragmentation, and runtime allocation failures. Caller-provided API buffers make memory ownership and capacity explicit while keeping the core usable on targets without a heap.

The full set, including the operating-mode and direction-encoding semantics, is in docs/design-decisions.md.


Usage Example

The example below shows the injection contract with a minimal blocking transport. The production STM32 port in Ports/sts_ports_stm32.c implements the same contract over DMA with IDLE-line half-duplex reception. Full API documentation is published at grish98.github.io/Feetech_Stm32.

#include "sts_servo.h"
#include "sts_servo_cmd.h"
/* 1. Implement your platform transport functions */
sts_result_t my_uart_tx(sts_bus_t *bus, const uint8_t *data, uint16_t len) {
UART_HandleTypeDef *huart = (UART_HandleTypeDef *)bus->port_handle;
return (HAL_UART_Transmit(huart, data, len, 10) == HAL_OK)
}
/* Distinguish a silent bus from a transport fault; both are retryable, but only
the caller can tell them apart afterwards. */
sts_result_t my_uart_rx(sts_bus_t *bus, uint8_t *data, uint16_t len, uint32_t timeout_ms) {
UART_HandleTypeDef *huart = (UART_HandleTypeDef *)bus->port_handle;
switch (HAL_UART_Receive(huart, data, len, timeout_ms)) {
case HAL_OK: return STS_OK;
case HAL_TIMEOUT: return STS_ERR_TIMEOUT;
default: return STS_ERR_RX_FAIL;
}
}
/* 2. Initialise the bus and servo handles */
STS_Bus_Init(&bus, &huart2, my_uart_tx, my_uart_rx);
bus.flush_rx = NULL; /* optional: supply one to drain stale RX between retries */
bus.max_retries = 2U; /* 0 = single attempt */
STS_Servo_Init(&servo, &bus, 0x01);
/* 3. Ping to confirm the servo is online */
if (STS_servo_ping(&servo) == STS_OK) {
/* servo.is_online == STS_ONLINE */
}
/* 4. Drive the servo. Every call returns a result code; a short chain keeps the
first failure rather than overwriting it. */
if (res == STS_OK) { res = STS_SetTorqueEnable(&servo, 1); }
if (res == STS_OK) { res = STS_SetTargetAcceleration(&servo, 50); }
if (res == STS_OK) { res = STS_SetTargetPosition(&servo, 2048); }
if (res == STS_ERR_TIMEOUT) {
/* Nothing answered: check wiring, servo ID, and baud rate. */
} else if (res == STS_ERR_HARDWARE) {
/* The servo replied but reports a fault, such as overload or overtemperature.
Communication is intact, so this is a servo-state problem, not a bus problem. */
}
/* 5. Read telemetry */
uint16_t pos = 0U;
uint8_t temp = 0U;
STS_GetPresentPosition(&servo, &pos);
/* 6. Read the bus counters at any time */
uint32_t failures = bus.hard_failures;
STS Shared Bus Handle.
Definition sts_servo.h:51
uint32_t hard_failures
Definition sts_servo.h:64
sts_hal_flush_rx_t flush_rx
Definition sts_servo.h:55
void * port_handle
Definition sts_servo.h:52
uint8_t max_retries
Definition sts_servo.h:57
STS Servo Handle Represents a single physical servo on the bus.
Definition sts_servo.h:74
sts_result_t
Return codes for STS protocol operations.
Definition sts_protocol.h:71
@ STS_ERR_HARDWARE
Definition sts_protocol.h:82
@ STS_ERR_TIMEOUT
Definition sts_protocol.h:77
@ STS_ERR_TX_FAIL
Definition sts_protocol.h:83
@ STS_OK
Definition sts_protocol.h:72
@ STS_MODE_POSITION
Definition sts_registers.h:133
: STS Service Layer and Hardware Abstraction Definitions
sts_result_t STS_Bus_Init(sts_bus_t *bus, void *port_handle, sts_hal_transmit_t tx_func, sts_hal_receive_t rx_func)
Initializes the hardware bus abstraction.
Definition sts_servo.c:183
sts_result_t STS_servo_ping(sts_servo_t *servo)
Pings the servo to check if it's online.
Definition sts_servo.c:217
sts_result_t STS_Servo_Init(sts_servo_t *servo, sts_bus_t *bus, uint8_t id)
Initializes a servo handle.
Definition sts_servo.c:196
: STS Servo Command API
sts_result_t STS_GetPresentTemperature(sts_servo_t *servo, uint8_t *temp_out)
Reads the internal temperature of the servo's MCU/Motor driver.
Definition sts_servo_cmd.c:197
sts_result_t STS_GetPresentPosition(sts_servo_t *servo, uint16_t *position_out)
Reads the current actual position from the servo.
Definition sts_servo_cmd.c:56
sts_result_t STS_SetOperatingMode(sts_servo_t *servo, sts_operating_mode_t mode)
Sets the operating mode of the servo.
Definition sts_servo_cmd.c:33
sts_result_t STS_SetTargetAcceleration(sts_servo_t *servo, uint8_t acceleration)
Sets the acceleration profile for servo movements.
Definition sts_servo_cmd.c:95
sts_result_t STS_SetTargetPosition(sts_servo_t *servo, uint16_t position)
Sets the absolute target position for the servo.
Definition sts_servo_cmd.c:45
sts_result_t STS_SetTorqueEnable(sts_servo_t *servo, uint8_t enable)
Enables or disables motor torque output.
Definition sts_servo_cmd.c:24

Building and Testing

Versioning

GitHub tags are the source of project versions; the badge above follows the latest tag automatically. On a push to main, successful host tests and static analysis allow CI to create the next tag: v0.1 for the first merge with this workflow, then v0.2, v0.3, and so on. Feature branches and pull-request checks do not create tags. Re-running CI for an already tagged commit keeps its existing version; superseded main commits are skipped. Documentation deployment runs separately and does not gate tagging.

Tags remain attached to their original commits. There are no manually maintained per-file version numbers, and dependency/tool versions are independent of the project version.

The library uses CMake with a dual-target build system. Host tests run on the development machine with a native compiler, so no hardware is required. The ARM firmware target is selected automatically when an arm-none-eabi toolchain is configured.

Prerequisites

  • CMake 3.22+
  • A C11 compiler (GCC or Clang)
  • CTest (included with CMake)

Build and run tests

cmake -B build_native
cmake --build build_native --config Debug
ctest --test-dir build_native -C Debug --output-on-failure

All 198 tests should pass across two suites: 40 in the protocol layer and 158 in the service and command layers. The --config and -C flags are required by multi-config generators such as Visual Studio and are ignored by single-config generators such as Ninja and Unix Makefiles.

Build the API documentation

Requires Doxygen. CI publishes the same output to GitHub Pages on every push to main.

cmake --build build_native --target docs

Generated HTML is written to build_native/html/index.html.


Protocol Summary

The Feetech STS protocol is a binary half-duplex UART protocol. Every packet follows this structure:

Byte(s) Field Description
0–1 Header Always 0xFF 0xFF
2 ID Servo ID (0–253, 254 = broadcast)
3 Length Number of remaining bytes (excl. header + ID)
4 Instruction Command or status byte
5..N Parameters Optional payload (0–253 bytes)
N+1 Checksum ~(ID + Length + Instruction + Params) & 0xFF

Responses are variable length, which is why the STM32 port frames them with UART IDLE-line detection rather than a fixed byte count. The servo supports four operating modes (position, speed, PWM, and step) selected through STS_SetOperatingMode; their semantics are described in docs/design-decisions.md.


Roadmap

  • [x] Protocol layer: packet framing, checksum, noise-resilient parsing
  • [x] Service layer: HAL-agnostic bus abstraction, command engine, register access primitives, ping, and command coverage for position, speed, acceleration, PWM, step, torque, telemetry, EEPROM, and ID
  • [x] STM32F103 port: DMA half-duplex with IDLE-line reception, hardware-validated
  • [x] Transient-hypothesis measurement: oscilloscope capture found no turnaround transient, so the defensive RX flush loop was retracted and removed (#10)
  • [x] Port simplification: PA2 stays in AF open-drain, removing per-packet GPIO switching (#10)
  • [x] Phase 1 functional non-regression: two 200-run AF_OD campaigns, using 1.5 kOhm to 3.3 V at 1 Mbaud
  • [ ] Phase 1 evidence closure: link the tested revision and complete campaign evidence; document settled-low voltage and receiver-threshold margin (#10)
  • [ ] Phase 2 receive-state hardening and its campaign gate (#10)
  • [ ] Phase 3 bounded error recovery, TX-completion review, documentation/compliance cleanup, and final campaign gate (#10)
  • [ ] Sync Write and Bulk Read support
  • [ ] Portable on-target test suite: route Hardware_Tests/ timing through the STS_Delay_ms and STS_GetTick_ms port hooks so the integration and stress suites can validate a new MCU port unmodified
  • [ ] Full-duplex bus-adapter path re-validation (#9)

Compatibility

The core (protocol and service layers) has no platform-specific dependencies and builds on any target with a C11 toolchain. Fixed-width integer types are used throughout for cross-architecture correctness. Porting to another MCU means supplying three transport callbacks, transmit, receive, and the optional flush_rx; no other platform code is required to drive servos. The porting guide documents the full contract, and the STM32F103 port in Lib/STS_Servo/Ports/ is a worked reference implementation.


License

Copyright (c) 2026 Grisham Balloo. All rights reserved.