Skip to content

charlesmmorais/ch32-servo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🌐 Português · Read in English

ch32-servo

Controlador de 8 servos hobby num chip RISC-V de ~R$ 0,50.
Escravo I²C · movimento suave · failsafe · poses sincronizadas · varredura diagnóstica

Licença: MIT MCU Framework Linguagem: C


                    ┌──────────────────────┐
  Raspberry Pi  ───►│  SDA (PC6)           │──► Servo 0 (PD2)
  Arduino       ───►│  SCL (PC5)           │──► Servo 1 (PC4)
  ESP32         ───►│                      │──► Servo 2 (PC3)
  qualquer      ───►│   CH32V003           │──► Servo 3 (PA1)
  mestre I²C        │   RISC-V  ~R$ 0,50  │──► Servo 4 (PC1)
                    │                      │──► Servo 5 (PD5)
                    │                      │──► Servo 6 (PD6)
                    └──────────────────────┘──► Servo 7 (PC7)

Por que este projeto existe

O CH32V003 tem dois timers de hardware. A maioria dos projetos de controlador de servo desperdiça os dois para gerar PWM contínuo em apenas 2 canais. Este firmware usa o modo one-pulse (OPM): cada pulso é disparado explicitamente por software a cada 20 ms, permitindo que cada timer sirva 4 canais por evento. Dois timers × 4 canais = 8 servos independentes num chip do tamanho de um grão de arroz.

Este projeto também é uma plataforma de aprendizado. Cada decisão de design está explicada no código-fonte.


Funcionalidades

Funcionalidade Detalhe
8 canais PWM independente a 50 Hz, faixa de 500–2500 µs
Resolução de 16 bits ≈ 41,7 ns por passo, ~0,1° de precisão angular
API em µs Escreva a largura de pulso em microssegundos — sem contas de clock no mestre
Movimento suave Limite de velocidade por canal; o servo desliza em vez de saltar
Poses sincronizadas Todos os canais chegam ao alvo simultaneamente (coreografia de robôs)
Failsafe Watchdog: vai para o centro se o mestre ficar em silêncio
Relaxar por canal Corta o PWM de qualquer canal (economiza energia, permite movimentação manual)
Leitura de status Consulte "este canal ainda está se movendo?" via I²C
Varredura diagnóstica Um comando executa mín→máx→centro em todos os servos
Saída intercalada TIM1 e TIM2 disparam com 10 ms de defasagem, reduzindo o pico de corrente
Boot seguro Todos os canais vão para 1500 µs antes do primeiro comando I²C
Limites rígidos O firmware limita todos os valores a [SERVO_MIN_US, SERVO_MAX_US]

Início rápido

1 — Obter o código

git clone --recurse-submodules https://github.com/charlesmmorais/ch32-servo

2 — Compilar e gravar

cd ch32-servo/src
make

Requer o toolchain do ch32fun.
O alvo padrão é flash (minichlink via WCH-Link).

3 — Conectar os servos

Pino CH32V003   Canal   Pino do conector
─────────────────────────────────────────
PD2             0       1
PC4             1       2
PC3             2       3
PA1             3       4
PC1             4       5
PD5             5       6
PD6             6       7
PC7             7       8

PC6  →  I²C SDA
PC5  →  I²C SCL
GND  →  GND (compartilhado com mestre e alimentação dos servos)

Atenção à alimentação: servos consomem muita corrente.
Alimente-os com uma fonte 5 V separada. Compartilhe apenas o GND com o CH32V003.

4 — Controlar pelo mestre

Arduino (Wire)

#include <Wire.h>

void setup() {
    Wire.begin();
    Wire.setClock(400000);

    // Move canal 0 para o centro, imediato
    Wire.beginTransmission(0x09);
    Wire.write(0x00); Wire.write(0xDC); Wire.write(0x05); // reg 0, 1500 µs LE
    Wire.endTransmission();

    // Move canal 0 para 900 µs com velocidade = 20 µs/frame (~1 s)
    Wire.beginTransmission(0x09);
    Wire.write(0x10); Wire.write(20); Wire.write(0);       // reg de velocidade
    Wire.endTransmission();
    Wire.beginTransmission(0x09);
    Wire.write(0x00); Wire.write(0x84); Wire.write(0x03); // 900 µs
    Wire.endTransmission();
}

MicroPython (Pico)

from machine import I2C, Pin
from docs.examples.micropython_master import MiteServo

i2c = I2C(0, sda=Pin(4), scl=Pin(5), freq=400_000)
s = MiteServo(i2c)

s.failsafe(5)               # vai para o centro se ficar 500 ms sem I²C
s.move(0, 1500)             # canal 0 → centro, imediato
s.move(1, 900, speed=20)    # canal 1 → 900 µs, devagar
s.wait(0x03)                # aguarda canais 0 e 1 terminarem
s.sync_move({0:800, 1:1200, 2:1800}, duration_ms=1000)  # pose coordenada

Exemplos completos em docs/examples/.


Mapa de registradores

O CH32V003 se apresenta como escravo I²C no endereço 0x09 (configurável em config.h).
Expõe um banco plano de 38 bytes. Todos os valores multi-byte são little-endian (byte menos significativo primeiro).

Posições e velocidades

Endereço Tam. L/E Descrição
0x000x0F 2 × 8 E Posição alvo, canais 0–7 (µs, 500–2500). Escrever 0x0000 = sem alteração.
0x100x1F 2 × 8 E Limite de velocidade, canais 0–7 (µs por frame de 20 ms). 0 = snap imediato.

Controle e status

Endereço Tam. L/E Descrição
0x20 1 E Máscara de habilitação — bit N = 1 habilita o canal N. 0 = relaxado (sem PWM).
0x21 1 L Status — bit N = 1 enquanto o canal N está se movendo em direção ao alvo.
0x22 1 E Timeout do failsafe — unidades de 100 ms. 0 = desabilitado.
0x23 1 E Controlebit 0 = SWEEP, bit 1 = SYNC (se limpa automaticamente).
0x240x25 2 E Duração do sync (ms, LE) — usado com CTRL_SYNC.

Como escrever uma posição

Escrita I²C: [ENDEREÇO_ESCRAVO | W]  [reg]  [val_lo]  [val_hi]

Mover canal 3 para 2000 µs (0x07D0):
  reg     = REG_TARGET(3) = 0x06
  val_lo  = 0xD0
  val_hi  = 0x07
  → sequência Wire.write: 0x06, 0xD0, 0x07

Como funciona (as partes interessantes)

Modo one-pulse — sem PWM contínuo

Timers convencionais de servo rodam a 50 Hz continuamente. Aqui, cada timer é configurado em modo OPM (one-pulse) + contagem decrescente:

 ATRLR = 0xFFFF ──►│ contador conta PARA BAIXO
                   │
           CCR ───►│──────────── saída ALTA enquanto contador > CCR
                   │
               0 ──►│ saída BAIXA, CEN se autolimpa, timer para

Setar TIM_CEN dispara exatamente um pulso. O ISR do SysTick rearma a cada 20 ms.
O timer fica ocioso entre os pulsos — sem jitter de recarga.

Bancos intercalados

Tempo: 0ms    10ms   20ms   30ms   40ms
       │       │       │       │       │
TIM1:  ████   ·       ████   ·       ████   (canais 0–3, 50 Hz)
TIM2:  ·       ████   ·       ████   ·       (canais 4–7, 50 Hz)
              ↑ defasagem de 10 ms

Disparar os dois bancos simultaneamente geraria um pico na alimentação. A defasagem de 10 ms distribui a carga.

Taxa de variação — slew rate (movimento suave)

Cada canal mantém current_us (o que o timer está gerando) e target_us (o que o mestre pediu). A cada frame, current_us avança em direção a target_us por no máximo speed_us:

// Passo por frame (simplificado)
int32_t delta = target_us - current_us;
if (abs(delta) <= speed_us)
    current_us = target_us;            // chegou
else
    current_us += sinal(delta) * speed_us;  // um passo mais perto

speed_us = 0 desabilita o slew (snap imediato).

Movimento sincronizado

Quando CTRL_SYNC é escrito, o firmware calcula uma velocidade por canal para que todos percorram suas distâncias no mesmo número de frames:

velocidade[canal] = |alvo[canal] - atual[canal]| / (sync_ms / 20)

Cada canal usa a mesma lógica de slew — sem código especial no ISR.

Watchdog failsafe

O callback de escrita I²C atualiza um timestamp. O ISR do SysTick compara com o tick atual. Se a diferença ultrapassar o limiar, servo_failsafe() é chamado e define todos os alvos como SERVO_CENTER_US com speed = 0 (imediato).


Configurando

Edite src/config.h:

#define I2C_ADDRESS      0x09    // Mude para usar várias placas no mesmo barramento
#define SERVO_MIN_US     500     // Piso rígido — o firmware nunca vai abaixo disso
#define SERVO_MAX_US     2500    // Teto rígido
#define SERVO_CENTER_US  1500    // Posição de boot e posição do failsafe

Nenhum outro arquivo precisa ser alterado para customizações típicas.


Estrutura do projeto

ch32-servo/
├── src/
│   ├── config.h        ← todos os parâmetros ajustáveis pelo usuário
│   ├── i2c_regs.h      ← mapa de registradores (defines + documentação do protocolo)
│   ├── servo.h/.c      ← máquina de estados do servo (sem hardware)
│   ├── main.c          ← init de hardware, timers, ISR, sweep e sync
│   ├── funconfig.h     ← seleção de chip para o ch32fun
│   └── Makefile
├── docs/
│   └── examples/
│       ├── arduino_master.ino       ← driver Arduino (Wire) com demo
│       └── micropython_master.py   ← driver MicroPython com demo
├── ch32fun/            ← submódulo (cnlohr/ch32fun)
├── README.md           ← este arquivo (Português)
├── README.en.md        ← versão em inglês
├── LICENSE
└── .gitmodules

Expandindo o projeto

Perfil de movimento trapezoidal

O slew atual é de velocidade constante. Para adicionar aceleração/desaceleração, substitua a lógica de passo em servo.c:servo_step_all() por uma máquina de estados que rampa speed_us no início e reduz perto do alvo. O ISR e o código de hardware não precisam de alteração.

Persistência em flash

Armazene calibração (mín/máx/trim por canal) na última página de flash usando os helpers de flash do ch32fun. Adicione um registrador REG_SAVE que dispara a gravação. Sobrevive a resets.

Endereço I²C configurável por pads

Exponha 2 pads de solda na PCB conectados a entradas GPIO. Leia-os no boot e combine com os bits inferiores de I2C_ADDRESS. Permite 4 placas no mesmo barramento (endereços 0x09–0x0C) sem regravação do firmware.

API em graus

Adicione uma camada de conversão no driver do mestre. O firmware permanece em µs intencionalmente — universalmente compreendido e independente do modelo do servo.


Limitações conhecidas

  • Leitura fragmentada — ler um registrador de alvo de 16 bits enquanto o mestre I²C está escrevendo o segundo byte pode produzir um glitch de um frame. Aceitável para servos hobby (20 ms se autocorrige). Adicione double-buffering para uso crítico de segurança.
  • Sem feedback de posição — o firmware não sabe onde o servo realmente está; ele só sabe qual pulso enviou. Sensoriamento externo de posição requer hardware adicional.
  • 38 bytes de espaço de registradores — o driver I²C slave do ch32fun usa um buffer de tamanho fixo. Estender o mapa de registradores é simples, mas requer ajustar I2C_REG_COUNT.

Compilando o toolchain

# Ubuntu / Debian
sudo apt install gcc-riscv64-unknown-elf make
git clone https://github.com/cnlohr/minichlink
cd minichlink && make
sudo cp minichlink /usr/local/bin/

# Neste repositório:
git submodule update --init
cd src && make

Testado com GCC 12 (riscv64-unknown-elf).


Créditos e relação com o projeto original

ch32-servo é uma reescrita completa do deshipu/mite-servo de @deshipu, a quem vai todo o crédito pela ideia central.

O que o original acertou de forma brilhante

O mite-servo introduziu um insight elegante: em vez de rodar PWM contínuo nos dois timers (o que daria apenas 2 canais), configurá-los em modo one-pulse e dispará-los explicitamente de um ISR do SysTick. Isso libera 4 canais por timer — 8 no total, em um chip sem controlador de servo nativo. A defasagem de 10 ms entre TIM1 e TIM2 para distribuir o pico de corrente também foi uma escolha deliberada e inteligente.

Essa arquitetura central está preservada aqui sem alterações. Se o original não existisse, este projeto também não existiria.

O que este projeto adiciona

O original é limpo e mínimo por design — uma prova de conceito funcional com ~200 linhas de C. Esta reescrita mantém o mesmo tamanho (dentro de 2 KB de RAM / 16 KB de flash) enquanto adiciona tudo que um deploy real precisa:

mite-servo ch32-servo
Unidade da API ticks brutos do timer microssegundos (portável)
Movimento suave snap imediato slew rate por canal
Poses sincronizadas todos os canais chegam juntos
Failsafe watchdog → centro no silêncio I²C
Relaxar canais desabilita PWM por canal
Leitura de status "este canal ainda está se movendo?"
Varredura diagnóstica mín → máx → centro por comando
Boot seguro CCR = 0 (pulso indefinido) todos os canais iniciam em 1500 µs
Limitação de valores nenhuma mín/máx rígidos no firmware
Mapeamento de pinos implícito (ordem de registradores = mistério) tabela conector→timer→pino documentada
Estrutura do código arquivo único lógica de servo separada do hardware
Exemplos para mestre Arduino + MicroPython com driver completo
Documentação README resumido docs de protocolo, cálculos de timing, racional de design

O que foi mantido igual de forma intencional

  • Alvo CH32V003 (sem razão para trocar um chip de R$ 0,50 que faz o trabalho)
  • Framework ch32fun e exemplo i2c_slave como base I²C
  • Configuração de timer OPM + contagem decrescente
  • Full remap do TIM2 (mesmos pinos: PC1, PC7, PD6, PD5)
  • Pinos I²C PC5/PC6 com high-bit remap
  • Endereço I²C padrão 0x09
  • Licença MIT

Crédito também a @cnlohr pelo ch32fun, sem o qual nada disso compilaria em menos de 30 segundos.


Licença

MIT — veja LICENSE. Faça o que quiser; uma menção é bem-vinda.

About

8-channel hobby servo controller for CH32V003 (RISC-V) over I²C

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors