Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Servo

PlatformIO Registry license ISC contributions welcome

RC servo control for CH32 RISC-V microcontrollers, built directly on ch32fun. No Arduino core, no HAL, no floating point, no dynamic allocation.

Builds for every CH32 family ch32fun supports that has a TIM2: CH32V003, CH32V00x, CH32V10x, CH32V20x, CH32V30x, CH32L103 and CH32X03x.

Only the CH32V003 has been run on hardware. Every other family's pin and register tables come from WCH reference manuals, cross-checked against a second source where one exists. They compile cleanly and are expected to work, but no servo has been driven by them. Board support gives the evidence per family; Known issues lists what is deliberately incomplete. A report from real silicon, working or not, is the most useful thing anyone could contribute right now.

The library has two interchangeable pulse-generation backends, chosen at build time. The API is identical under both, so switching is one #define and a pin change:

#include "Servo.h"

int main(void)
{
    SystemInit();

    Servo servo;
    servo.attach(SERVO_TIM2_CH1_PIN);  // TIM2 CH1 for this chip and remap group
    servo.writeMicroseconds(1500);     // centre

    while (1) Delay_Ms(1000);       // the timer holds the position for you
}

Why two backends

TIM2 is the only general-purpose timer on a CH32V003, and plenty of libraries want it, including Ticker, which owns it outright. A servo library that only works by taking TIM2 is unusable in exactly the projects that need it most.

TIM2 backend (default) SysTick backend (SERVO_USE_SYSTICK)
Pulse generated by the timer peripheral a compare-match interrupt
Interrupts used none¹ SysTick compare
Timers consumed TIM2 none
Servos up to 4, one per channel² 8+, limited by frame time
Pins TIM2 channel pins only any GPIO
Pulse accuracy exact, immune to your code ±10 µs, subject to interrupt latency
Flash 224 B 484 B

¹ Unless SERVO_SMOOTH is enabled, which needs the TIM2 update interrupt to advance ramps.

² Four on most families. Some remap groups route fewer: see the TIM2 pin table. Flash figures are CH32V003, from Flash and RAM.

Pick the TIM2 backend unless something else in your firmware needs TIM2, or you need more than four servos, or you need a pin that is not a TIM2 channel.

Features

  • Optional smooth motion: write() sets a target and returns, and the position ramps there from the interrupt the active backend already runs. Nothing to poll in your main loop.
  • Microsecond and integer-degree APIs. The degree API compiles out if unused.
  • Integer-only, no dynamic allocation, no runtime pin tables, no runtime validation. 224 B of flash and 8 B per servo at its smallest.

Prerequisites

  • A CH32 chip with a TIM2, and ch32fun.
  • A servo. Give it its own supply, because a hobby servo's stall current will brown out an MCU sharing its rail, and tie the grounds together. Signal is 3.3 V from these parts; most servos accept that, some want 5 V logic.

Installation

PlatformIO

[env:genericCH32V003F4P6]
platform = https://github.com/Community-PIO-CH32V/platform-ch32v.git
board = genericCH32V003F4P6
framework = ch32v003fun

lib_deps = fyfar/Servo

fyfar/Servo is the registry name. The source lives under the ch32-libraries GitHub organisation, which is a separate namespace.

For another chip, change board to one the CH32V platform provides. The library picks the family up from the framework's own -D, so nothing else in the .ini changes. examples/pio_servo_sweep is a complete standalone project.

By hand, with a Makefile

Copy src/Servo.h and src/Servo.cpp into your project, then:

TARGET_MCU?=CH32V003          # ch32fun.mk has no default and errors without one
ADDITIONAL_C_FILES+=path/to/Servo.cpp

include path/to/ch32fun/ch32fun.mk

# Must come after the include: ch32fun.mk sets CFLAGS with `?=`, so setting it
# beforehand makes that assignment a no-op and silently drops -Os and -flto.
CFLAGS+=-Ipath/to/servo/src

API

bool     attach(uint8_t pin);           // ch32fun pin constant; false if registry full
void     detach();                      // stops pulses, leaves the line LOW
void     writeMicroseconds(uint16_t us);// clamped to SERVO_MIN_US..SERVO_MAX_US
uint16_t readMicroseconds() const;      // the commanded position, after clamping

void     write(uint8_t angle);          // SERVO_ENABLE_ANGLE (on by default)

void     setSpeed(uint16_t us_per_sec); // SERVO_SMOOTH; 0 = move immediately
bool     isMoving() const;              // SERVO_SMOOTH

writeMicroseconds() is safe before attach(): the value is retained and applied when the servo attaches. Copying a Servo is a compile error, and a Servo going out of scope detaches itself.

A note on file-scope servos

Servo servo; at file scope works, but its constructor only runs if you set FUNCONF_SUPPORT_CONSTRUCTORS 1 in funconfig.h. Without it the object is zeroed in BSS and starts at SERVO_MIN_US instead of centre: harmless, but not what you wrote. A Servo declared inside a function needs nothing.

Configuration

Every knob is a build-time #define, in funconfig.h or as -D. There are no runtime tables and no runtime validation: a wrong define gives you wrong hardware, not a diagnostic. Where a mistake is statically detectable the build fails and says so.

Macro Default Meaning
SERVO_USE_SYSTICK 0 1 selects the SysTick backend
SERVO_SMOOTH 0 1 compiles in ramped motion
SERVO_ENABLE_ANGLE 1 0 drops write(degrees)
SERVO_FRAME_US 20000 frame period, µs (20000 = 50 Hz)
SERVO_MIN_US 1000 pulse width at minimum position
SERVO_MAX_US 2000 pulse width at maximum position
SERVO_ANGLE_MAX 180 degree span mapped onto that range
SERVO_MAX_SERVOS 4 registry size
SERVO_TIM2_REMAP 0 TIM2 output remap group; legal range is per family (see below)

Clock requirements

  • TIM2 backend: core clock must be a whole multiple of 1 MHz.
  • SysTick backend: core clock at least 8 MHz, or at least 1 MHz with FUNCONF_SYSTICK_USE_HCLK=1.

Any FUNCONF_USE_HSI/FUNCONF_USE_HSE and FUNCONF_PLL_MULTIPLIER choice that meets these is fine; every default ch32fun ships for a supported chip already does. An unsafe combination fails at build time rather than mis-timing your servo.

Calibrate the range

1000-2000 µs is a nominal figure, not a fact about your servo. Real units vary; many accept 500-2500 µs and give you noticeably more travel, and two servos of the same model can differ. SERVO_MIN_US/SERVO_MAX_US is the calibration knob. Sweep slowly outward and back off when the servo stalls or buzzes at the endpoint.

TIM2 pin table

The remap group is a property of the timer, not of one servo: it moves all four channels at once. Choose the group that covers the pins you want, then attach to pins from that row only.

Which pins a group provides depends on the family, so Servo.h exposes SERVO_TIM2_CH1_PIN..SERVO_TIM2_CH4_PIN for whichever family and group is configured. The examples use those. Passing a pin outside the configured row fails at link time by design; the pin must be a compile-time constant.

Each family's tier and the documents behind its table are in Board support.

What each family gives you:

Family Legal SERVO_TIM2_REMAP Max servos on TIM2 Notes
CH32V003 0-3 4 package may cost you pins, see below
CH32V10x, CH32V20x, CH32V30x 0-3 4
CH32L103 0-3 4 register supports 4-7; this library does not
CH32X03x 0-3, 5-7 4 in groups 0 and 2, otherwise 3 group 4 unusable; CH1 unavailable in 1, 3, 5-7
CH32V00x 0-7 4

A group with no usable channel, or a group number the family does not define, fails the build with a message naming the problem rather than silently picking another. Needing more servos than the table allows is what the SysTick backend is for. It uses any GPIO and has no such limit.

CH32V003 (hardware-verified)

SERVO_TIM2_REMAP CH1 CH2 CH3 CH4
0 (default) PD4 PD3 PC0 PD7
1 PC5 PC2 PD2 PC1
2 PC1 PD3 PC0 PD7
3 PC1 PC7 PD6 PD5

PD7 is also NRST. To use CH4 in groups 0 or 2 you must first disable the reset function via the option bytes (minichlink -d).

Package matters. TSSOP20/QFN20 (CH32V003F4P6/F4U6) expose all 18 GPIO, so every row above is usable. SOP16 (CH32V003A4M6, 14 GPIO) omits PD0, PD2, PD3 and PC5, which makes remap group 1 unusable there (it needs both PC5 and PD2) and costs CH2 in groups 0 and 2. SOP8 (CH32V003J4M6, 6 GPIO) is more restricted still. Check your package's pinout before choosing a remap group.

CH32V10x, CH32V20x, CH32V30x, CH32L103 (cross-checked)

All four carry the same table.

SERVO_TIM2_REMAP CH1 CH2 CH3 CH4
0 (default) PA0 PA1 PA2 PA3
1 PA15 PB3 PA2 PA3
2 PA0 PA1 PB10 PB11
3 PA15 PB3 PB10 PB11

No pin here doubles as a debug or reset line. Debug is two-wire on PA13/PA14, so PA15 and PB3 are ordinary GPIO and groups 1 and 3 are usable. ch32fun's AFIO_PCFR1_SWJ_CFG macros suggest otherwise, but they are an ST leftover: WCH's manuals give that field as SWD-only and never mention JTAG.

CH32L103 stops at group 3. Its remap field is {TIM2_RM_H, TIM2_RM}, and the third bit lives in AFIO->PCFR2[21], which this library leaves at reset. Reaching groups 4 to 7 needs a second, L103-only register write. Group 6 is not defined by the manual at all.

CH32X03x (cross-checked)

SERVO_TIM2_REMAP CH1 CH2 CH3 CH4
0 (default) PA0 PA1 PA2 PA3
1 - † PB15 PA2 PA3
2 PA0 PA1 PB3 PB4
3 - † PB15 PB3 PB4
4 - † - † - † - †
5 - † PA12 PA13 PC0
6, 7 - † PC14 PC15 PC0

† Unreachable through ch32fun. CH32X035's GPIO ports are 24 bits wide and these channels land on PB21, PB16-PB19 or PC19, but ch32fun's pin constants stop at 15 per port and its GpioOf() treats pin 16 as the next port. Group 4 has no usable channel at all. Fixing this means extending ch32fun.

No usable pin here doubles as a debug or reset line. RST is on PA21, PC3 or PB7 depending on package, PC17 is the BOOT detection pin, and SWD is on PC18/PC19, the latter being a CH1 that cannot be named anyway.

CH32V00x: CH32V002, CH32V004-007 (manual only)

CH32M007 shares this table and Servo.h handles it, but ch32fun has no TARGET_MCU for that part, so it cannot be built today.

SERVO_TIM2_REMAP CH1 CH2 CH3 CH4
0 (default) PD4 PD3 PC0 PD7
1 PC1 PD3 PC0 PD7
2 PC5 PC2, or PB3 ⚠ on V007/M007 PD2 PC1
3 PC1 PC7 PD6 PD5
4 PC0 PC1 PC3 PB6
5 PA0 PA1 PA2 PA3
6 PB1 PA1 PA2 PA3
7 PD3 PD4 PA2 PA3

⚠ Three pins carry a second function. PD7 is also RST, as on CH32V003, so CH4 in groups 0 and 1 needs the reset function disabled in the option bytes. PB3 is SWCLK, and is a TIM2 channel only on CH32V007 and CH32M007, which put CH2 there instead of PC2. That split is the only per-part difference in any table here, and the manual documents it. PA1 and PA2 are the XI/XO crystal pins, gated by AFIO_PCFR1.PA1PA2_RM, so they are usable as GPIO only when no external crystal is fitted.

Smooth motion

servo.setSpeed(1000);              // 1000 µs of pulse width per second
servo.writeMicroseconds(2000);     // returns immediately
while (servo.isMoving()) { }       // optional

The ramp is advanced once per frame from whichever interrupt the active backend already runs: the TIM2 update interrupt, or the SysTick handler. Your main loop does not have to call anything.

Rates are quantised to whole microseconds per frame, so they come in 50 µs/s increments at the default 50 Hz and the slowest non-zero rate is 50 µs/s (a full 1000 µs sweep in 20 seconds). A rate too small to move one microsecond per frame is rounded up to one rather than silently standing still.

Examples

Example What it shows
servo_sweep one servo on TIM2, plain microsecond sweep
servo_multi four servos on the four TIM2 channels (needs a family and group that provides all four)
servo_systick three servos on the SysTick backend, on non-timer pins
servo_smooth ramped motion with an idle main loop
pio_servo_sweep standalone PlatformIO project
cd examples/servo_sweep && make

Flash and RAM

Measured on CH32V003 at -Os -flto, as the increase over an empty SystemInit()-only firmware. "Static RAM" is the library's own state; each Servo object costs sizeof(Servo) on top, wherever you put it.

Configuration Flash Static RAM sizeof(Servo)
TIM2 backend 224 B 4 B 8 B
TIM2 + degree API 324 B 4 B 8 B
TIM2 + smoothing 688 B 20 B 10 B
SysTick backend 484 B 32 B 6 B
SysTick + degree API 556 B 32 B 6 B
SysTick + smoothing 640 B 32 B 8 B

Smoothing costs the most because it adds an interrupt handler, not because of the ramp math. The degree API costs about 100 B because this core has no hardware multiply, so set SERVO_ENABLE_ANGLE=0 if you only speak microseconds. No soft-float or allocator symbols appear in any configuration.

Impact on the chip

The TIM2 backend claims TIM2, its four channels and the AFIO->PCFR1 remap field. It sets the prescaler for a 1 MHz tick and the auto-reload for a 20 ms frame, so a compare register holds the pulse width in microseconds directly, and enables no interrupt unless SERVO_SMOOTH is set. On CH32X03x it also sets MOE in TIM2->BDTR, because TIM2 there is an advanced-control timer whose outputs are off at reset. No other family has that register.

The SysTick backend claims only the compare-match interrupt vector. It never resets the counter, changes its clock source or enables auto-reload, so Delay_Us(), funSysTick32() and anything else reading SysTick->CNT keep working, and TIM2 is never touched. Servos are pulsed one at a time, so every pulse must fit inside one frame. That caps the backend near nine servos, checked at build time.

Limitations

  • The TIM2 backend cannot coexist with any other TIM2 user, including Ticker. This is what "uses TIM2" means, not a bug we can fix. Use the SysTick backend, which is exactly why it exists.
  • The SysTick backend claims the SysTick interrupt vector, so it collides with a hand-rolled millis() or anything else wanting that vector.
  • SysTick-backend pulse width degrades with interrupt latency. Code that disables interrupts, or a higher-priority handler that runs long, shows up as servo jitter. Use the TIM2 backend if you cannot accept that.
  • Configuration mistakes are yours to catch. Only statically detectable ones fail the build. A legal-but-wrong SERVO_TIM2_REMAP will happily drive the wrong pin. This is a deliberate trade for size and is the ch32fun convention.
  • One pulse-width range for all servos. Mixed servos wanting different ranges should use writeMicroseconds() directly.
  • Not every family gives four TIM2 channels in every remap group. CH32X03x loses CH1 in five of its seven usable groups and has one group with none at all. CH32L103 is limited to groups 0-3. CH32M007 cannot be built at all. Known issues has the reasons.

Board support

CH32V003 was checked with a scope, logic analyzer and WCH-LinkE: both backends, pulse widths and clamping, detach and re-attach, four servos sequenced on non-timer pins, frame stability, ramp rate, mid-flight retargeting, and register read-back confirming the SysTick backend never enables TIM2's clock.

Every other family carries the evidence its support actually rests on:

  • Hardware-verified: confirmed by measurement on real silicon.
  • Cross-checked: a WCH reference manual plus an independent corroborating source.
  • Manual only: a reference manual with nothing to check it against.
Chips SysTick backend TIM2 backend What the TIM2 table rests on
CH32V003 Hardware-verified Hardware-verified CH32V003RM + examples/tim2_pwm_remap, run on hardware
CH32V10x Cross-checked (its SysTick is shaped differently and handled accordingly) Cross-checked CH32xRM table 10-11 + ch32v10xhw.h
CH32V20x (all packages, incl. D8/D8W), CH32V30x Cross-checked Cross-checked CH32FV2x_V3xRM table 10-16 + ch32v20xhw.h/ch32v30xhw.h
CH32L103 Cross-checked Cross-checked, groups 0-3 only CH32L103RM §10 + CH32L103DS0 pin table
CH32X03x Cross-checked Cross-checked, some channels unreachable CH32X035RM §8 + CH32X035DS0 + examples_x035/tim2_pwm
CH32V002, CH32V004-007 Cross-checked Manual only CH32V00XRM §9, no second source except on V006
CH32M007 - - Servo has its table, but ch32fun has no TARGET_MCU for it yet
CH32H41x Not supported Not supported Incompatible clock and interrupt architecture
CH5xx family (CH551, CH552, CH570-CH592) Not supported (no TIM2) Not supported (no TIM2) -

All MCU-dependent code sits in one marked portability block in Servo.h: a per-family descriptor chain, then one table of four pin defines per family and remap group. Building for a family with no TIM2 register layout fails at compile time with a message pointing at that block rather than misconfiguring registers.

Known issues and remaining work

Deliberately incomplete, in rough order of how much anyone is likely to care:

  • CH32X03x channels on pins above P?15. CH32X035's GPIO ports are 24 bits wide, but ch32fun's pin constants stop at 15 per port and its GpioOf() treats pin 16 as the next port. That costs CH1 in remap groups 1, 3, 5, 6 and 7, and all four channels in group 4. Fixing it means extending ch32fun's GPIO helpers first.
  • CH32L103 remap groups 4-7. Needs AFIO->PCFR2[21] handling, which is a second, L103-only register write. Group 6 is not defined by the manual at all.
  • CH32M007 cannot be built. Servo.h has its table, but ch32fun has no TARGET_MCU for that part.
  • CH32V00x has no second source. Its tables come from CH32V00XRM alone; only the CH32V006 datasheet was available to check pinouts against.

Issues and pull requests

https://github.com/ch32-libraries/Servo

License

ISC. See LICENSE.

About

RC servo control for CH32V-family RISC-V microcontrollers, built directly on ch32fun. Two interchangeable timing backends (TIM2 PWM or SysTick), optional smooth motion.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages