Europe/Istanbul
All Projects

Basic Manipulator on PIC — Multi-Servo Arm with PIC16F887

A servo-driven manipulator built on the PIC16F887 with the CCS C compiler. Notes on timing, power integrity, and the minimal interrupt scheduler that drives multiple hobby servos from a single MCU.
August 9, 2025
PIC16F887
CCS C
PICkit
PWM
Servos
Embedded
Robotics
“If you understand time, you can move anything.” — lab notes
A basic robotic manipulator controlled by hobby servos. The controller is a Microchip PIC16F887, programmed via PICkit and the CCS C toolchain.
Project goals were: reliable multi-channel servo PWM, smooth motion from joystick/knob inputs, and a robust power layout that survives lab abuse.
Assembled arm on the bench

  • MCU: PIC16F887 (internal 8 MHz RC).
  • Outputs: up to 6 servo channels on PORTC with 1–2 ms pulses @ 50 Hz.
  • Inputs: pots/joystick on ADC; buttons for preset poses.
  • Power: dedicated 5 V BEC for servos + separate 5 V LDO for logic; common ground, TVS + bulk caps near the servo rail.
  • Programming: PICkit via ICSP (MCLR header on the board).
Hand-wired schematic on a Partinax prototyping board

Using CCPx PWM with Timer2 is convenient, but not ideal for 50 Hz frames across many channels.
I implemented a lightweight Timer1 interrupt scheduler instead:
  • Timer1 tick = 4 µs (Fosc/4 with prescaler 1:8 @ 8 MHz).
  • Servo pulse high: 250–500 ticks (1–2 ms).
  • Frame: 20 ms (≈ 5000 ticks).
  • ISR walks channels: set pin HIGH, wait width, set LOW, advance; the last slot inserts the remaining frame gap.
// CCS C — PIC16F887
#include <16F887.h>
#fuses  INTRC_IO, NOWDT, NOLVP, NOMCLR, BROWNOUT
#use    delay(clock=8MHz)

#define CHN 6
const int1 SERVO_PIN[CHN] = {PIN_C0,PIN_C1,PIN_C2,PIN_C3,PIN_C4,PIN_C5};
volatile unsigned int16 width[CHN];   // 4us ticks (250..500)
volatile int8 ch = -1;

#INT_TIMER1
void t1_isr() {
  set_timer1(0);
  if(ch >= 0 && ch < CHN) output_low(SERVO_PIN[ch]);
  ch++;
  if(ch < CHN) {                       // start next channel HIGH
    output_high(SERVO_PIN[ch]);
    set_timer1(65535 - width[ch]);     // end after pulse width
  } else {                              // frame gap
    unsigned int16 sum=0; for(int8 i=0;i<CHN;i++) sum+=width[i];
    set_timer1(65535 - (5000 - sum));  // 20ms - total HIGH time
    ch = -1;
  }
}

void main() {
  setup_oscillator(OSC_8MHZ);
  setup_timer_1(T1_INTERNAL | T1_DIV_BY_8);
  enable_interrupts(INT_TIMER1); enable_interrupts(GLOBAL);

  // neutral: 1.5ms -> 375 ticks
  for(int8 i=0;i<CHN;i++) width[i]=375;

  while(TRUE) {
    // read ADC → map to 250..500 ticks
    // apply slew-rate limit for smooth motion
  }
}
Why it works: one timer deterministically times all channels; the main loop stays free for ADC reads and simple kinematics; jitter is low because there’s no busy-wait.
  • Timing discipline: measuring real pulse widths on a scope and budgeting the 20 ms frame.
  • Power integrity: isolating servo surges from logic rails; bulk + decoupling placement; short return paths.
  • CCS C practice: #INT_TIMER1, setup_timer_1, precise integer timing; EEPROM for trims; clean startup at neutral.
  • Bring-up workflow: PICkit ICSP, safe boot, brown-out recovery, quick reflash without disassembling the rig.

  • Add encoder feedback for the wrist with a simple per-joint position loop.
  • A small PC tuner to record/recall poses and play sequences.
  • Consider PIC18/dsPIC if we need more channels or trajectory planning.
Takeaway: A lean Timer1 ISR is a reliable way to drive many hobby servos on small PICs. This project sharpened my embedded timing, power design, and CCS C tooling on real hardware.