From install to a blinking LED, and everything after.
PICPIO is a VS Code extension. Install it like any other:
Or press Ctrl+P and run:
ext install picpio.picpio
To build and flash real hardware, you also install Microchip's free tools (one time). PICPIO opens the right download pages for you on first run.
Check everything is found anytime by running picpio doctor in the PICPIO terminal.
PIC18F27K40), the programmer, and the framework:
TRISx/LATx/PORTx).src/main.c, a picpio.ini, and a REFERENCE.md with your chip's pin map and API.A PICPIO program has two functions: init() runs once at boot, and run() repeats forever.
#include <Picpio.h>
void init() { // runs once at startup
gpio_mode(BUILTIN_LED, GPIO_OUT);
}
void run() { // repeats forever
gpio_write(BUILTIN_LED, GPIO_HIGH);
sys_delay(500);
gpio_write(BUILTIN_LED, GPIO_LOW);
sys_delay(500);
}
That's a complete blink program. Pins are named D0–Dn, A0–An for analog, and BUILTIN_LED for the on-board LED.
Simple, consistent functions across every supported chip. It's plain C, compiled by XC8/XC16/XC32/XC-DSC exactly like any other library, not a custom language. The uart1.begin(...)-style calls work because each peripheral is a struct of function pointers under the hood.
| Function | What it does |
|---|---|
gpio_mode(pin, mode) | GPIO_IN, GPIO_OUT, or GPIO_PULLUP |
gpio_write(pin, val) | GPIO_HIGH or GPIO_LOW |
gpio_read(pin) | Returns GPIO_HIGH or GPIO_LOW |
Pins are named D0–Dn and A0–An (the exact range depends on your chip), plus BUILTIN_LED for the on-board LED. Prefer the chip's own pin names (RA0, RC2…)? Put #define PICPIO_PIN_ALIASES before #include <Picpio.h> and those work too.
| Function | What it does |
|---|---|
adc_read(pin) | Reads an analog pin. Resolution depends on the chip (10-bit on most PIC16/PIC32, 12-bit on dsPIC33A…), your project's REFERENCE.md has the exact range. |
pwm_write(pin, duty) | PWM duty cycle, 0–255, at each chip's fixed default frequency |
pwm_config(pin, freq_hz, resolution_bits) | Set a specific PWM frequency (Hz) and duty resolution (up to 16 bits) before using pwm_write16(), ESP32 ledcSetup()-style. On chip families where one timer drives several PWM pins, this sets the frequency for all of them at once, not just the pin you passed, your project's REFERENCE.md says which. |
pwm_write16(pin, duty) | Duty scaled to whatever resolution the last pwm_config() call asked for on that pin |
adc_pwm_write(adc_read(pin), pwm_pin, ascending) | Scales an ADC reading onto pwm_pin's configured duty range and writes it. ascending nonzero: ADC low → duty low. Zero: inverted (ADC low → duty high). A potentiometer or light sensor can drive an LED/motor/buzzer in one call. |
| Function | What it does |
|---|---|
sys_delay(ms) | Block for whole milliseconds |
sys_delay_us(us) | Block for whole microseconds |
sys_millis() | Milliseconds since boot |
sys_micros() | Microseconds since boot |
uart1, and uart2 on chips with a second hardware UART:
| Function | What it does |
|---|---|
uart1.begin(baud) / uart1.end() | Start or stop the port |
uart1.print(x) / uart1.println(x) | Send a string, number or float, same call for every type |
uart1.write(byte) | Send one raw byte |
uart1.available() / uart1.read() | Bytes waiting, and read one |
uart1.flush() | Wait for the send buffer to empty |
| Function | What it does |
|---|---|
i2c1.begin() | Start the bus as controller |
i2c1.beginTransmission(addr) / i2c1.endTransmission() | Wrap a write to a device address |
i2c1.requestFrom(addr, len) | Ask a device for len bytes |
i2c1.write(byte) | Queue a byte inside a transmission |
i2c1.available() / i2c1.read() | Bytes waiting, and read one |
| Function | What it does |
|---|---|
spi1.begin() / spi1.end() | Start or stop the bus |
spi1.transfer(byte) | Send a byte, returns the byte read back |
spi1.setBitOrder(...) | SPI_MSB or SPI_LSB |
spi1.setDataMode(...) | SPI_MODE0–SPI_MODE3 |
spi1.setClockDivider(...) | SPI_CLOCK_DIV2–SPI_CLOCK_DIV128 |
spi1/i2c1 for you. You'll rarely touch these directly, except when writing your own driver.Global enable/disable only, there's no attach-a-callback API:
sys_irq_on(); // enable
sys_irq_off(); // disable
Familiar helpers ship too: byte/word/boolean typedefs, PI, min/max/constrain/map, and bit_read/bit_set/bit_clr/bit_write.
Example: read a sensor and print it:
void init() {
uart1.begin(115200);
}
void run() {
int v = adc_read(A0); // range depends on your chip
uart1.println(v);
sys_delay(200);
}
REFERENCE.md lists the exact pin map, ADC resolution, and the full API for your chip, open it anytime.Use the PICPIO toolbar buttons, or the terminal:
Connect your board with a PICkit (or Snap/ICD), hit Upload, and watch it flash:
$ picpio upload
Compiling main.c with XC8...
Program: 3.1% Data: 1.4%
✓ Flashed to PIC18F27K40
✓ Done in 4.2s
Every one of PICPIO's 130+ libraries ships a complete, runnable example, the same code picpio lib add drops into your sketch. Read it before you commit to a library, not after installing it.
PICPIO ships 130+ ready-made drivers: OLED/TFT displays, sensors, motor & stepper drivers, RTCs, ADC/DAC expanders, RF/CAN/LoRa, USB CDC serial, GPS/GSM, keypads, SD card (FAT16/32), EEPROM, WS2812 LEDs, servos, PID and more.
REFERENCE.md.$ picpio lib add SSD1306
✓ Added SSD1306 (I2C OLED)
✓ Example inserted into src/main.c
Once you've added a library, PICPIO already knows what you're using. Open Wiring Diagram from the sidebar and it reads your src/main.c to draw exactly how to connect it, no datasheet hunting.
Detected from main.c:
✓ BME280 → I2C1 (RC3 SCL, RC4 SDA)
✓ ILI9341 → SPI1, CS on RC0
✓ XPT2046 touch → SPI1, CS on RC1
It updates as you add more libraries, and the same wiring gets written into your project's REFERENCE.md, so it's there even with the panel closed.
Open Pin Map from the sidebar to see every pin on your exact chip package, DIP or QFP, labeled with both its friendly PICPIO name (D0, A0…) and its native port pin (RB0, RA0…).
REFERENCE.md as plain text, alongside the full API for your chip.Building a UI on a TFT or OLED? Open Display Designer from the sidebar:
No board yet? Run your logic in the built-in simulator and watch it work.
src/main.c.Great for teaching, for checking logic, and for working without hardware in hand.
Run Simulate Network (server + clients) to host the simulator as a server, then use Share Simulator Pages on My Network to open it from any other device on your network, no PICPIO install needed on that device. Handy for a classroom, or showing someone a demo without sending them a file.
See live output from your board and send data back:
uart1.begin(...)).COM3 @ 115200
Temp: 24.6 C
Temp: 24.7 C
Humidity: 48%
Teaching or learning register-level PIC programming? Pick the bare-metal framework when creating a project. You write a normal main() against the chip's real SFRs, with full IntelliSense (autocomplete on TRISB, PORTAbits, …).
// bare-metal blink
void main(void) {
TRISBbits.TRISB0 = 0; // RB0 output
while (1) {
LATBbits.LATB0 ^= 1; // toggle
__delay_ms(500);
}
}
Every project includes a REFERENCE.md generated for your exact chip:
D0, A0, the I2C/SPI/UART pins…).It's read-only and always matches your hardware, keep it open as you build.
XC8/XC16/XC32 error messages are often cryptic. PICPIO recognizes the common ones and shows what actually happened and what to do about it, in plain English, right in the Problems panel and the terminal, no error-code lookup needed.
If a build or upload fails, run picpio doctor: it checks your whole toolchain and tells you what's missing:
$ picpio doctor
✓ XC8 compiler v2.46
✓ PICkit 3 detected
✓ Device pack installed
✓ 130+ libraries
All systems go.
picpio install-dfp once, then build again.