diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index b088f28f4d4a6a8cd810433089afa1c1a8686d9c..fe3796b257422291f987f66aab73530f5daa60d3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -35,25 +35,16 @@ jobs:
     runs-on: ${{ matrix.os }}
     steps:
     - uses: actions/checkout@v3
-    - name: Cache pip
-      uses: actions/cache@v3
+    - uses: actions/cache@v3
       with:
-        path: ~/.cache/pip
-        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
-        restore-keys: |
-          ${{ runner.os }}-pip-
-    - name: Cache PlatformIO
-      uses: actions/cache@v3
+        path: |
+          ~/.cache/pip
+          ~/.platformio/.cache
+        key: ${{ runner.os }}-pio
+    - uses: actions/setup-python@v4
       with:
-        path: ~/.platformio
-        key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
-    - name: Set up Python
-      uses: actions/setup-python@v4
-      with:
-        python-version: "3.9"
-    - name: Install PlatformIO
-      run: |
-        python -m pip install --upgrade pip
-        pip install --upgrade platformio
-    - name: Run PlatformIO
-      run: pio run
+        python-version: '3.9'
+    - name: Install PlatformIO Core
+      run: pip install --upgrade platformio
+    - name: Build PlatformIO Project
+      run: pio run
\ No newline at end of file
diff --git a/README.md b/README.md
index a833ad0aeec93fe21e5c967fcb0c14db8b3439d0..fe987373a5e339f2c9b0b92d0cf3598834d58def 100644
--- a/README.md
+++ b/README.md
@@ -112,15 +112,20 @@ To use the WCH-Link in WSL, it is required to "attach" the USB hardware on the W
 9. For unknown reasons, you must run make under root access in order to connect to the programmer with minichlink.  Recommend running `sudo make` when building and programming projects using WSL. This may work too (to be confirmed):
 
 ### non-root access on linux
-Unlike serial interfaces, by default, the USB device is owned by root, has group set to root and everyone else may only read.
-1. Get the vendor:device ID of the WCH-Link from `lsusb`.
-2. Create a udev rule with `sudo nano /etc/udev/rules.d/80-USB_WCH-Link.rules`, paste (CTRL+SHIFT+V) `SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="8010", MODE="0666"` and save, replacing the idVendor and idProduct if what you got previously was different.
-3. Reboot or reload the udev rules with `sudo udevadm control --reload-rules && sudo udevadm trigger` 
-4. ???
-5. profit
-Now anyone on your PC has access to the WCH-Link device, so you can stop using sudo for make.  
-I don't think there are any security risks here.
-You may also tie this to the WCH-Link serial number or some other attribute from `udevadm info -a -n /dev/bus/usb/busid/deviceid` with the bus id and device id you got from lsusb earlier.
+Unlike serial interfaces, by default, the USB device is owned by root, has group set to root and everyone else may only read by default.
+The way to allow non-root users/groups to be able to access devices is via udev rules.
+
+minichlink provides a list of udev rules that allows any user in the plugdev group to be able to interact with the programmers it supports.
+
+You can install and load the required udev rules for minichlink by executing the following commands in the root of this Git repository:
+```
+sudo cp minichlink/99-minichlink.rules /etc/udev/rules.d/
+sudo udevadm control --reload-rules && sudo udevadm trigger
+```
+
+If you add support for another programmer in minichlink, you will need to add more rules here.
+
+**Note:** This readme used to recommend manually making these rules under `80-USB_WCH-Link.rules`. If you wish to use the new rules file shipped in this repo, you may want to remove the old rules file.
 
 
 ## minichlink
diff --git a/examples/GPIO/Makefile b/examples/GPIO/Makefile
index 399d16eba5d704e10e1ce2a4c0455daf5b06477c..87347371d38e65e7c05f84225b553f957d5d1eca 100644
--- a/examples/GPIO/Makefile
+++ b/examples/GPIO/Makefile
@@ -3,7 +3,7 @@ all : flash
 TARGET:=GPIO
 
 CFLAGS+=-DTINYVECTOR
-ADDITIONAL_C_FILES+=wiring.c
+ADDITIONAL_C_FILES+=../../extralibs/wiring.c
 
 include ../../ch32v003fun/ch32v003fun.mk
 
diff --git a/examples/GPIO_analogRead/Makefile b/examples/GPIO_analogRead/Makefile
index e47191ea6a2a9efea0eb37aa371f0c276ec5f817..a965d90a0e3e624237b8b7234cfce7e09475c44e 100644
--- a/examples/GPIO_analogRead/Makefile
+++ b/examples/GPIO_analogRead/Makefile
@@ -3,7 +3,7 @@ all : flash
 TARGET:=GPIO_analogRead
 
 CFLAGS+=-DTINYVECTOR
-ADDITIONAL_C_FILES+=wiring.c
+ADDITIONAL_C_FILES+=../../extralibs/wiring.c
 
 include ../../ch32v003fun/ch32v003fun.mk
 
diff --git a/examples/GPIO_analogRead/wiring.c b/examples/GPIO_analogRead/wiring.c
deleted file mode 100644
index e0acde1977e48cc74ac45f7b33c123d25e028758..0000000000000000000000000000000000000000
--- a/examples/GPIO_analogRead/wiring.c
+++ /dev/null
@@ -1,238 +0,0 @@
-//#include <stdio.h>
-
-#include "wiring.h"
-#include <stdint.h>
-
-
-
-enum GPIOports getPort (enum GPIOpins pin) {
-	if (pin <= pin_A2) {
-		return port_A;
-	}
-	else if (pin <= pin_C7) {
-		return port_C;
-	}
-	else if (pin <= pin_D7) {
-		return port_D;
-	}
-	return port_none;
-}
-
-
-
-void portEnable(enum GPIOports port) {
-	// Enable GPIOs
-	switch (port) {
-		case port_A:
-			RCC->APB2PCENR |= RCC_APB2Periph_GPIOA;
-			break;
-		case port_C:
-			RCC->APB2PCENR |= RCC_APB2Periph_GPIOC;
-			break;
-		case port_D:
-			RCC->APB2PCENR |= RCC_APB2Periph_GPIOD;
-			break;
-		case port_none:
-			break;
-	}
-}
-
-
-
-void pinMode(enum GPIOpins pin, enum GPIOpinMode mode) {
-	GPIO_TypeDef * GPIOx;
-	uint16_t PinOffset;
-
-	GPIOx = GPIOA+(pin>>3);
-	PinOffset = (pin & 0x7)<<2;
-
-	GPIOx->CFGLR &= ~(0b1111<<PinOffset);							// zero the 4 configuration bits
-	
-	uint8_t target_pin_state = pinState_nochange;					// by default, pin shall retain its state
-
-	uint8_t modeMask = 0;												// configuration mask
-
-	switch (mode) {
-		case pinMode_I_floating:
-			modeMask = GPIO_CNF_IN_FLOATING;
-			break;
-		case pinMode_I_pullUp:
-			modeMask = GPIO_CNF_IN_PUPD;
-			target_pin_state = pinState_high;
-			break;
-		case pinMode_I_pullDown:
-			modeMask = GPIO_CNF_IN_PUPD;
-			target_pin_state = pinState_low;
-			break;
-		case pinMode_I_analog:
-			modeMask = GPIO_CNF_IN_ANALOG;
-			break;
-		case pinMode_O_pushPull:
-			modeMask = GPIO_Speed_10MHz | GPIO_CNF_OUT_PP;
-			break;
-		case pinMode_O_openDrain:
-			modeMask = GPIO_Speed_10MHz | GPIO_CNF_OUT_OD;
-			break;
-		case pinMode_O_pushPullMux:
-			modeMask = GPIO_Speed_10MHz | GPIO_CNF_OUT_PP_AF;
-			break;
-		case pinMode_O_openDrainMux:
-			modeMask = GPIO_Speed_10MHz | GPIO_CNF_OUT_OD_AF;
-			break;
-	}
-
-	// wirte mask to CFGR
-	GPIOx->CFGLR |= modeMask<<PinOffset;
-
-	// set pin state
-	if (target_pin_state > pinState_nochange) {
-		digitalWrite(pin, target_pin_state - 1);
-	}
-}
-
-
-
-void digitalWrite(enum GPIOpins pin, uint8_t value) {
-	// no checks given whether pin is currently being toggled by timer! your output trannys are in your hands! beware the magic smoke!
-	GPIO_TypeDef * GPIOx;
-	uint16_t PinOffset = 0;
-	
-	if (pin <= pin_A2) {
-		GPIOx = GPIOA;
-		PinOffset = pin;
-	}
-	else if (pin <= pin_C7) {
-		GPIOx = GPIOC;
-		PinOffset = (pin - 16);
-	}
-	else if (pin <= pin_D7) {
-		GPIOx = GPIOD;
-		PinOffset = (pin - 24);
-	}
-	else {
-		return;
-	}
-
-	if (value) {
-		GPIOx-> BSHR |= 1 << PinOffset;
-	}
-	else {
-		GPIOx-> BSHR |= 1 << (16 + PinOffset);
-	}
-}
-
-
-
-uint8_t digitalRead(uint8_t pin) {
-	GPIO_TypeDef * GPIOx;
-	uint16_t PinOffset = 0;
-	
-	if (pin <= pin_A2) {
-		GPIOx = GPIOA;
-		PinOffset = pin;
-	}
-	else if (pin <= pin_C7) {
-		GPIOx = GPIOC;
-		PinOffset = (pin - 16);
-	}
-	else if (pin <= pin_D7) {
-		GPIOx = GPIOD;
-		PinOffset = (pin - 24);
-	}
-	else {
-		return 0;
-	}
-
-	int8_t result = (GPIOx->INDR >> PinOffset) & 1;
-	return result;
-}
-
-
-
-
-
-void ADCinit() {
-	// select ADC clock source
-	// ADCCLK = 24 MHz => RCC_ADCPRE = 0: divide by 2
-	RCC->CFGR0 &= ~(0x1F<<11);
-
-	// enable clock to the ADC
-	RCC->APB2PCENR |= RCC_APB2Periph_ADC1;
-
-	// Reset the ADC to init all regs
-	RCC->APB2PRSTR |= RCC_APB2Periph_ADC1;
-	RCC->APB2PRSTR &= ~RCC_APB2Periph_ADC1;
-
-	// set sampling time for all inputs to 241 cycles
-	for (uint8_t i = Ain0_A2; i <= AinVcal; i++) {
-		ADCsetSampletime(i, Ast_241cy_default);
-	}
-
-	// set trigger to software
-	ADC1->CTLR2 |= ADC_EXTSEL;
-
-	// pre-clear conversion queue
-	ADC1->RSQR1 = 0;
-	ADC1->RSQR2 = 0;
-	ADC1->RSQR3 = 0;
-
-	// power the ADC
-	ADCsetPower(1);
-}
-
-
-
-void ADCsetSampletime(enum ANALOGinputs input, enum ANALOGsampletimes time) {
-	// clear
-	ADC1->SAMPTR2 &= ~(0b111)<<(3*input);
-	// set
-	ADC1->SAMPTR2 |= time<<(3*input);	// 0:7 => 3/9/15/30/43/57/73/241 cycles
-}
-
-
-
-void ADCsetPower(uint8_t enable) {
-	if (enable) {
-		ADC1->CTLR2 |= ADC_ADON;
-		if (enable == 1) {
-			// auto-cal each time after turning on the ADC
-			// can be overridden by calling with enable > 1.
-			ADCcalibrate();					
-		}
-	}
-	else {
-		ADC1->CTLR2 &= ~(ADC_ADON);
-	}
-}
-
-
-
-void ADCcalibrate() {
-	// reset calibration
-	ADC1->CTLR2 |= ADC_RSTCAL;
-	while(ADC1->CTLR2 & ADC_RSTCAL);
-	
-	// calibrate
-	ADC1->CTLR2 |= ADC_CAL;
-	while(ADC1->CTLR2 & ADC_CAL);
-}
-
-
-
-// inspired by arduinos analogRead()
-uint16_t analogRead(enum ANALOGinputs input) {
-	// set mux to selected input
-	ADC1->RSQR3 = input;
-
-	// may need a delay right here for the mux to actually finish switching??
-	// Arduino inserts a full ms delay right here!
-	
-	// start sw conversion (auto clears)
-	ADC1->CTLR2 |= ADC_SWSTART;
-	
-	// wait for conversion complete
-	while(!(ADC1->STATR & ADC_EOC));
-	
-	// get result
-	return ADC1->RDATAR;
-}
diff --git a/examples/GPIO_analogRead/wiring.h b/examples/GPIO_analogRead/wiring.h
deleted file mode 100644
index 3b7d0d74e0a1a8f1d41fca115b4aaeb3901cc8e2..0000000000000000000000000000000000000000
--- a/examples/GPIO_analogRead/wiring.h
+++ /dev/null
@@ -1,99 +0,0 @@
-#ifndef WIRING_H
-#define WIRING_H
-
-#include "ch32v003fun.h"
-
-
-
-enum lowhigh {
-	low,
-	high,
-};
-
-
-
-enum GPIOports{
-	port_A,
-	port_C,
-	port_D,
-	port_none,
-};
-
-enum GPIOpins{
-	pin_A1 = 1,
-	pin_A2,
-	pin_C0 = 16,
-	pin_C1,
-	pin_C2,
-	pin_C3,
-	pin_C4,
-	pin_C5,
-	pin_C6,
-	pin_C7,
-	pin_D0 = 24,
-	pin_D1,
-	pin_D2,
-	pin_D3,
-	pin_D4,
-	pin_D5,
-	pin_D6,
-	pin_D7,
-	pin_none,
-};
-
-enum GPIOpinMode {
-	pinMode_I_floating,
-	pinMode_I_pullUp,			//pull-mode + ODR(1)
-	pinMode_I_pullDown,			//pull-mode + ODR(0)
-	pinMode_I_analog,
-	pinMode_O_pushPull,
-	pinMode_O_openDrain,
-	pinMode_O_pushPullMux,
-	pinMode_O_openDrainMux,
-};
-
-enum GPIOpinState {
-	pinState_nochange,
-	pinState_low,
-	pinState_high,
-};
-
-enum ANALOGinputs {
-	Ain0_A2,
-	Ain1_A1,
-	Ain2_C4,
-	Ain3_D2,
-	Ain4_D3,
-	Ain5_D5,
-	Ain6_D6,
-	Ain7_D4,
-	AinVref,
-	AinVcal,
-};
-
-enum ANALOGsampletimes {
-	Ast_3cy,
-	Ast_9cy,
-	Ast_15cy,
-	Ast_30cy,
-	Ast_43cy,
-	Ast_57cy,
-	Ast_73cy,
-	Ast_241cy_default,
-};
-
-
-enum GPIOports getPort (enum GPIOpins pin);
-
-void portEnable(enum GPIOports port);
-void pinMode(enum GPIOpins pin, enum GPIOpinMode mode);
-void digitalWrite(enum GPIOpins pin, uint8_t value);
-uint8_t digitalRead(uint8_t pin);
-
-void ADCinit();
-void ADCsetPower(uint8_t enable);
-void ADCsetSampletime(enum ANALOGinputs input, enum ANALOGsampletimes time);
-void ADCcalibrate();
-uint16_t analogRead(enum ANALOGinputs input);
-
-#endif					// WIRING_H
diff --git a/examples/GPIO/wiring.c b/extralibs/wiring.c
similarity index 98%
rename from examples/GPIO/wiring.c
rename to extralibs/wiring.c
index e0acde1977e48cc74ac45f7b33c123d25e028758..b04f8e46dc07a1832b4332edbc3a0e44b02e427b 100644
--- a/examples/GPIO/wiring.c
+++ b/extralibs/wiring.c
@@ -43,7 +43,7 @@ void pinMode(enum GPIOpins pin, enum GPIOpinMode mode) {
 	GPIO_TypeDef * GPIOx;
 	uint16_t PinOffset;
 
-	GPIOx = GPIOA+(pin>>3);
+	GPIOx = (GPIO_TypeDef *)(((uint8_t*)(GPIOA))+(pin>>3)*0x0400);
 	PinOffset = (pin & 0x7)<<2;
 
 	GPIOx->CFGLR &= ~(0b1111<<PinOffset);							// zero the 4 configuration bits
diff --git a/examples/GPIO/wiring.h b/extralibs/wiring.h
similarity index 100%
rename from examples/GPIO/wiring.h
rename to extralibs/wiring.h
diff --git a/minichlink/99-minichlink.rules b/minichlink/99-minichlink.rules
index db763c84e4072c6488d9af1125889153f90a8e48..8cca6989adad49aceff2f035b04e8024dc76446e 100644
--- a/minichlink/99-minichlink.rules
+++ b/minichlink/99-minichlink.rules
@@ -1,4 +1,4 @@
-SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="8010", GROUP="plugdev", MODE="0666"
-SUBSYSTEM=="usb", ATTR{idVendor}=="303a", ATTR{idProduct}=="4004", GROUP="users", MODE="0666"
-KERNEL=="hidraw*", SUBSYSTEM=="hidraw", MODE="0664", GROUP="plugdev"
+SUBSYSTEM=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="8010", GROUP="plugdev", MODE="0660"
+SUBSYSTEM=="usb", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="4004", GROUP="plugdev", MODE="0660"
+KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="303a", ATTRS{idProduct}=="4004", GROUP="plugdev", MODE="0660"
 
diff --git a/platformio.ini b/platformio.ini
index b1626df25cd0b6ed47b99b90832676c03463dcd3..263b6ac00e167e52572d7cda8edff404de3f7e12 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -17,7 +17,13 @@ extends = fun_base
 board_build.ldscript = ch32v003fun/ch32v003fun.ld
 build_flags = -flto -Ich32v003fun -I/usr/include/newlib -DTINYVECTOR -lgcc -Iextralibs
 build_src_filter = +<ch32v003fun>
+extra_libs_srcs = +<extralibs>
 
+; If creating a new example:
+; 1. Add new [env:name]
+; 2. Add build_src_filter with fun base files + example folder (+ extra libraries if used) for source files
+; 3. Add additional build flags as needed (see uartdemo)
+; 4. Switch to new environment in VSCode bottom taskbar (https://docs.platformio.org/en/latest/integration/ide/vscode.html#project-tasks)
 [env:blink]
 build_src_filter = ${fun_base.build_src_filter} +<examples/blink>
 
@@ -30,6 +36,12 @@ build_src_filter = ${fun_base.build_src_filter} +<examples/debugprintfdemo>
 [env:external_crystal]
 build_src_filter = ${fun_base.build_src_filter} +<examples/external_crystal>
 
+[env:GPIO]
+build_src_filter = ${fun_base.build_src_filter} ${fun_base.extra_libs_srcs} +<examples/GPIO>
+
+[env:GPIO_analogRead]
+build_src_filter = ${fun_base.build_src_filter} ${fun_base.extra_libs_srcs} +<examples/GPIO_analogRead>
+
 [env:optionbytes]
 build_src_filter = ${fun_base.build_src_filter} +<examples/optionbytes>