jump to navigation

Build your own 2JZDuino v0.2 2012/12/12

Posted by Michael in 2JZduino.
add a comment

For anybody interested in building their own 2JZDuino shield or even using the design as reference for your own creation, here are some technical details on the hardware.

  1. Eagle .brd (board) and .sch (schematic) files can be downloaded here Рuse your favorite PCB fab shop and follow their instructions to convert these into circuit boards. I used http://dorkbotpdx.org/wiki/pcb_order/
  2. Reference my Bill Of Materials for populating the components.

I have not to-date documented the schematic for the LCD display very well, but if you search around the Arduino community there is plenty of information there on how it’s done.

 

2JZDuino 0.5 Beta (public release), and Arduino EEPROM 2012/07/16

Posted by Michael in 2JZduino.
1 comment so far

I’ve posted a minor update to 2JZDuino. Get it from the 2JZDuino project page at SourceForge .
Download “2JZDuino 0.5 beta.zip”.

Included in the .zip package is the project folder (including source-code) for the Windows EEPROM programmer I wrote to support 2JZDuino; IS300_Arduino_EEPROM.

The only changes I implemented since the 0.4.x release are some adjustments to target a more stable idle and acceleration. For a long time I’ve had an issue where MAF turbulence caused the injector pulse-widths to vary significantly at idle. I implemented some logic that intervenes at idle if the injector signal leans-out too quickly.

The acceleration change was simply based on the volumetric efficiency (VE) I’ve estimated for my engine. Minimum injector Pulse-Width enforced during tip-in is now calculated for a higher assumed VE.

0.4.1 Alpha (4th public release) 2011/08/17

Posted by Michael in 2JZduino.
3 comments

I’ve posted another release of 2JZDuino at Sourceforge, available here

This release comes after an extensive amount of testing and a few minor code changes & tweaks. As of this release I’ve logged about 10,000 km operating my IS300 with the 2JZduino connected as a piggyback ECU. For a large part of it I’d been fighting with a lean AFR condition that seems to occur either at low RPM when the throttle is partially depressed (i.e. tip-in), or at idle. I’ve speculated that there is an issue with the stability of the MAF reading due either to the larger intake tube diameter I have installed (3.5″ diam. vs. 2.75″ stock diam.), or turbulence around the MAF sensor due to the shortened intake tract.

There are two symptoms…

1) At idle the AFR would sometimes randomly drift lean to ~17:1 or rich to ~11:1 before the short-term fuel trims would correct.

2) For low RPMs and partial throttle the AFR would drift lean to upwards of 18:1. This was often observed when accelerating gently (MAP readings ~80kPa) from idle RPMs. Under this condition the ECU seems to stay in closed-loop operation which doesn’t provide enough fuel enrichment when the throttle first begins to open. The result is a significant loss of torque when trying to accelerate from low RPM. More throttle would trigger open-loop behaviour and the ECU would command the correct fuel for ~13:1 AFR. But part-throttle often resulted in a fuel shortage until the O2 sensors would command the short-term fuel trims to recover (or driver throttle input triggered closed-loop condition).

This highlighted a potential risk for when the Supercharger is installed… part throttle could result in positive manifold pressures which when mixed with lean AFR conditions could result in engine damage.

For both problems I implemented some logic in the h_InjectorChange() function that intervenes with the injector pulse width if a lean AFR condition is detected. The code is as follows…

  if (!InjectorEventState[Inj_Index]) { // only intervene with InjOFF events
    unsigned int thisInjPWcmd_T64 = TCNT4 - T64_LastInjON[Inj_Index]; // calculate the commanded PW for this Injector and for this combustion cycle
    if ((Inj_Index < 3 && AirFuelRatioB1_x10 > 160) || (Inj_Index >= 3 && AirFuelRatioB2_x10 > 160)) { // this Injector # is part of a bank that is currently LEAN
      // Sometimes at tip-in/accel. from idle a lean condition occurs (for MAP >~60kPa) -> Add fuel to obtain the expected amount
      if (!EngineIsStarting && ManifoldAirPressure > 60 && thisInjPWcmd_T64 < ExpectedIdleTipInInjPW_T64[MAPindex]) EventDelay += ExpectedIdleTipInInjPW_T64[MAPindex] - thisInjPWcmd_T64;
      // Sometimes AFR drifts lean at steady-idle -> Add 1/2 difference in fuel between previous injection dwell and Normal Idle fuel amount
      else if (ManifoldAirPressure >= 27 && EngineHz < 22 && thisInjPWcmd_T64 < NormalIdleInjPW_T64) EventDelay += (NormalIdleInjPW_T64 - thisInjPWcmd_T64) >> 1;
    }
  }

Effectively, if the AFR is ever detected to be leaner than 16:1, the logic will intervene and add to the injector pulse width: an amount to drive the AFR to stoichiometric AFR for MAP > 60 kPa, and half the amount to drive AFR to stoichiometric for the idle condition. The stoichiometric amount is pre-calculated based on an assumed volumetric efficiency of 50% (a reasonable estimate for the situation where these lean conditions tend to occur).

The logic is not intended to enrich the fuel past stoichiometric because the stock ECU fuel control still needs an opportunity to work. The idea is simply to enrich the fuel mixture temporarily while the stock closed-loop system responds.

For the idle condition the enrichment is only half of the difference between the expected and actual pulse-width (allowing the stock closed-loop control more of an opportunity to intervene). Generally the idle AFR fluctuations are more slow-moving, while the part-throttle lean condition is a fleeting condition requiring more aggressive intervention.

There is no intervention for MAP < 27 kPa, which is the point where the engine is deemed to be in vacuum and is decelerating (i.e. driver throttle input is zero).

Other changes since the v0.3 release…

  • Volumetric Efficiency considerations added to the calculation of InjectorOffsetsT64[][]. The volumetric efficiency for each point on the RPM vs. MAP table is now considered when the stoichiometric fuel amount is estimated (refer to this post). This should provide a more accurate fuel adjustment from the Injector Scaling map page of IS300_Arduino_EEPROM.exe.
  • New logic added that tracks EngineIsStarting. When the engine first starts to crank a maximum injector pulse-width is imposed to prevent over-enrichment in the case where larger injectors are installed. The stock ECU commands a lot of fuel during engine cranking. When test fitting my 440cc/min injectors I found this to result in much too much fuel during engine start. IS300_Arduino_EEPROM.exe now allows you to set the “Max Cranking AFR” which calculates a maximum pulse-width for 100% VE for the “Fuel Injector Size” value.
  • Logic implemented to intervene with lean conditions at idle, and under part-throttle (per the elaborate description above).
  • I’ve had some limited time to test the operation of 2JZduino with larger fuel injectors. I was limited by what seems to have been a small air leak between the injectors and the intake manifold, leading the engine to behave like there was a vacuum leak at low RPM. I’ve run into delay after delay in resolving this mechanical issue, so further testing will have to wait for now.

Volumetric Efficiency 2011/04/15

Posted by Michael in 2JZduino.
6 comments

Early on in developing 2JZduino I made an assumption that the Injector Pulse length for each combustion cycle was exclusively a function of Manifold Pressure. The reasoning was that independent of all other variables the volumetric efficiency of the air intake system would remain mostly constant and a certain Manifold Pressure correlates strongly to the volume of air in the combustion chamber.

This is grossly incorrect. In hindsight it’s obvious that the engine replaces air at varying efficiencies (which explains things like why there is such a strong torque curve in a 4-stroke engine), but I was mistaken in the magnitude of the effect. And being so large, the effect of VE has a significant impact on fuel trims (which were previously assuming VE ~= 90%), upwards of 400% under some engine conditions.

To go about resolving this I logged a fair amount of engine data intentionally exercising a range of engine speeds and manifold air pressures so that I could calculate volumetric efficiency in post processing. The datalogger object in the 2JZduino code measures Engine Hz, MAP, AFR (banks 1 & 2), and Injector Pulse Length every 167ms. With this information I have measurements for the amount of air in the combustion chamber (calculated from AFR and Injected Fuel amount), and quantity of air for 100% efficiency (manifold pressure). The actual volumetric efficiency for each data-point is then calculated as the ratio of the two. The calculation looks like this…

2JZduino datalogger Measurements:
Hz = instantaneous engine speed
MAP = instantaneous manifold air (absolute) pressure, kPa
AFR = Air Fuel Ratio (latent)
T_inj = injector pulse length for previous combustion cycle, ms

Constants and Knowns:
Q_inj = 3.32 g/s = fuel injector flow rate
Inj_Latency = 1 ms = opening time of the injector solenoid
V_cyl = 0.5L = cylinder volume (swept)
CR = 10.5 = engine compression ratio
d_Air = 1.25 g/L = approximate air density
P_atm = 101 kPa = atmospheric pressure

Solving for the combustion chamber air mass that corresponds to 100% volumetric efficiency:
V_chamber = 0.5 + 0.5/(10.5-1) = 0.553L = total volume of combustion chamber
M_air_100% = 0.553 * d_Air = 691mg = air mass for 100% VE and MAP = P_atm

Solving for the instantaneous air mass in the combustion chamber for the instantaneous measurements: e.g. for Hz = 38, MAP = 66 (absolute, T_inj = 5.0, AFR = 14.1
M_fuel = (T_inj – Inj_Latency) * Q_inj = 13.3 mg
M_air = M_fuel * AFR = 186.5 mg

Solving for measured VE:
VE = M_air / (M_air_100% * MAP/P_atm) = 186.5 / (691 * 66/101) = 41.3%

Using these calculations for every logged data point I was then able in post processing to begin separating/sorting/analyzing the relationship between engine speed, manifold pressure, and volumetric efficiency. I eventually arrived at the following formula empirically that calculates volumetric efficiency (percent) as a function of engine speed and manifold pressure…

Eq. #1)
VE(Hz, MAP) = 12 + (Hz * MAP)/320 + MAP/2.67 – Hz/16 + 100/Hz
…constrained as VE > 20% and VE > 90%

This relationship was then used to calculate and predict (rather than measure) the volumetric efficiency for each data point in the datalog, and then further predict the pulse length that would be required for the measured AFR. Finally the error is calculated between actual injector pulse length and predicted injector pulse length. Continuing from the example above the calculation is as follows…
VE(38, 66) = 44.8%
M’_fuel = (V_chamber * d_Air * MAP/P_atm)/AFR * VE(Hz, MAP) = 14.4 mg
T’_inj = Inj_Latency + M’_fuel/Q_inj = 5.3 ms
Pulse Length Error = 5.3 – 5.0 = 0.3 ms

As verification to the empirical VE formula, the graph below shows select portions of the data-logs that present the measured and calculated (using Eq. #1) injector pulse lengths alongside the error between them. For reference the corresponding engine MAP is also shown.

Clearly, the errors between calculated and measured injector pulse lengths are held mostly to less than 1ms. Exceptions to this occur primarily under conditions where injector pulse-lengths change rapidly (caused by rapid changes in throttle position). In these instances the response times of sensors and even the 2ZJduino are the likely sources of error.

This particular calculation for VE (Eq. #1) shows to be a rather good approximation. Now with an understood of volumetric efficiency for my particular 2JZ-GE, next steps are to apply the calculated VE values to the fuel trim map in 2JZduino. More on this in a future post.

For reference, below is a map of measured Volumetric Efficiency of my 2JZduino for Engine Hz vs. Manifold Air Pressure…

UnoEngineSim and (hacked) Interrupt Priorities on Arduino Mega 2011/04/05

Posted by Michael in 2JZduino.
add a comment

During testing I began to experience a misfire that was only occurring above about 5500 RPM. I wondered if the igniter signals were sometimes being delayed due to over-lapping interrupts that were higher priority. I’d recently purchased an Uno as a second Arduino and so I wrote an engine simulator that would run on the Uno to test my theory.

The Uno was configured to produce Injector and Igniter output signals at the intervals typical of a 2JZ-GE engine running at about 6000RPM. These outputs were connected to the inputs of 2JZduino. One-at-a-time the signals *output* by 2JZduino were connected back into the Uno on Pin8; the Input Capture pin, so that the latency of 2JZduino could be measured. The Uno would then report out statistics on the time from when the simulated output signal was generated, to the time that 2JZduino recreated the signal (captured by the ATmega 168’s Timer1 Input Capture register).

Below is a copy of the EngineSim code run on the Uno. Once a second, it reports out the # of events, minimum/maximum/average event delays, and the number of events that fell below the average (providing an estimate of the spread of the data).

// Pin assignments
// Pin2(PD2) = Inj1sim
// Pin3(PD3) = Inj2sim
// Pin4(PD4) = Inj3sim
// Pin5(PD5) = Inj4sim
// Pin6(PD6) = Inj5sim
// Pin7(PD7) = Inj6sim

// Pin8(PB0) = Input Capture

// Pin9(PB1) = IGT1sim
// Pin10(PB2) = IGT2sim
// Pin11(PB3) = IGT3sim

// ********************
// Establish all timing for engine speed of ~100Hz = 6000RPM (28us / deg)
// ********************

volatile unsigned int tEventQueue; // time the event was queued
volatile unsigned int tEventDelayMax; // max elapsed time from Event-queued to Input-Capture
volatile unsigned int tEventDelayMin; // min elapsed time from Event-queued to Input-Capture
volatile unsigned long tEventDelaySum;
volatile unsigned int tEventCount;
volatile unsigned int tEventCountSmall;
volatile unsigned int tEventDelayAverage;

void MonitorInput()
{
  TCCR1B |= B10000000; // Noise Canceler = ON, Capture Falling Edges
  TIMSK1 |= B00100000; // enable Input Capture interrupt
  DDRB &= B11111110; // B0 is an input
  PORTB |= B00000001; // B0 pull-up resistor ON
}

void StartIGTandInjSim()
{
  DDRB |= B00001110; // IGT1..3 sim on PB1..3
  PORTB &= B11110001; // Ensure IGT1..3 signals are OFF
  DDRD |= B11111100; // Inj1..6 sim on PD2..7
  PORTD |= B11111100; // turn on Inj1..6sim -> signal is active low
  TCCR1A = 0; // Normal operating mode
  TCCR1B = B00000010; // clk/8 -> 32.77ms roll-over
  TIMSK1 |= B00000110; // enable OCIE1B and OCIE1A 
}

void StopIGTandInjSim()
{
  PORTB &= B11110001; // Ensure IGT1..3 signals are OFF
  PORTD |= B11111100; // turn on Inj1..6sim -> signal is active low
  TIMSK1 = 0;
}

ISR(TIMER1_COMPA_vect) // IGT sim
{
  // Toggle Sequence: 1,1,2,2,3,3
  const byte IGTbitmaskSeq[6] = { B00000010, B00000010, B00000100, B00000100, B00001000, B00001000 };
  static byte IDX = 0;

  if (IDX == 1) tEventQueue = TCNT1; // Queue the event for IGT1 ON
  PINB = IGTbitmaskSeq[IDX]; // toggle in sequence
  IDX++;
  if (IDX >= 6) IDX = 0;
  
  OCR1A += 5247; // Next event: 3250/65536 * 32.77ms = 1.62ms -> 3 IGT pulses (equally spaced) each revolution
}

ISR(TIMER1_COMPB_vect) // INJ sim
{
  PIND = B11111100; // toggle all the injectors simultaneously
  OCR1B += 10003; // Next event: 10000/65536 * 32.77ms = 5ms -> Sim. all Injectors pulse each revolution
}

ISR(TIMER1_CAPT_vect)
{
  unsigned int Delay = ICR1 - tEventQueue;
  if (Delay > tEventDelayMax) tEventDelayMax = Delay; // store the peak delay that occurred
  if (Delay < tEventDelayMin) tEventDelayMin = Delay; // store minimum delay that occurs
  if (Delay < tEventDelayAverage) tEventCountSmall++;
  tEventDelaySum += Delay;
  tEventCount++;
}

void StartSim()
{
    StartIGTandInjSim();
    MonitorInput();
}

void StopSim()
{
  StopIGTandInjSim();
  tEventDelayMax = 0;
}

void setup() {
  Serial.begin(115200);
  Serial.println("IGT1 ON Event Delay in clk/8 counts...");
  MonitorInput();

  StartSim();
  Serial.println("Begin...");
}

void loop() {
  if (Serial.available())
  {
    char c = Serial.read();
    
    if (c == 's') {
      StartSim();
      Serial.println("Begin...");
    }
    else if (c == 'x') {
      StopSim();
      Serial.println("stop.");
    }
  }
  if (tEventDelayMax > 0) 
  {
    Serial.print(tEventDelayMin, DEC);
    Serial.print(" .. ");
    Serial.print(tEventDelayMax, DEC);
    Serial.print(" (");
    Serial.print(tEventDelayAverage, DEC);
    Serial.print(":");
    Serial.print(tEventCount, DEC);
    Serial.print(":");
    Serial.print(tEventCountSmall, DEC);
    Serial.println(")");
  }
  else Serial.print(".");
  tEventDelayAverage = tEventDelaySum/tEventCount;
  tEventDelayMax = 0;
  tEventDelayMin = 65535;
  tEventDelaySum = 0;
  tEventCount = 0;
  tEventCountSmall = 0;
  delay(1000);
}

Note that the Injector and IGT signals are intentionally setup on different frequencies so they drift in and out of phase in over-lap. This was done to ensure a worst-case scenario would eventually surface where the injector events, ADC_complete events, timer overflows, etc. would all interfere with the IGT events. What I found is that on some occasions the igniter signals would be delayed by upwards of 600us. At 5500 RPM this translates to about 20 degrees. The Uno simulator was written as worst-case (with all 6 injectors firing simultaneously), but it was at least seeming plausible that under some circumstances interrupt conflicts could cause the igniter signal to be delayed long enough that a misfire could occur because the engine spark arrived too late.

Looking at the datasheet for the Atmega 1280, the Interrupt vectors show that the IGT interrupt requests (Pin Change Interrupt Requests 0/1/2) are lower in priority than the Injector interrupt requests (External Interrupt Request 0/1/2/3/4/5). What I wanted was for the IGT interrupts to be treated with higher priority than the Injector interrupts. Spark events are critical in timing, but getting slightly more or less fuel would have a negligible effect. The solution I arrived at was to configure the interrupt handlers for the Injector External Interrupts like this…

ISR(INT0_vect, ISR_NOBLOCK) { // Ext Interrupt 0 on D0
  noInterrupts();
  myIS300.h_InjectorChange(0);
  interrupts();
};

In this way with the “ISR_NOBLOCK” argument, if all 6 Injector interrupts and an IGT interrupt occurred simultaneously the external interrupts would all queue up on the stack, each one interrupting the other, until the IGT interrupt gets serviced as the highest priority interrupt. The IGT interrupt, being a blocking interrupt, would be fully-serviced, and then the Injector interrupts would start getting popped off the stock one-by-one, unwinding until normal program flow continued.

With this change the maximum delay on the IGT signal was reduced from 600us down to 40us. 95% of the time the IGT signal delay was < 20us. This translates to better than 1deg of latency in the IGT signal at 6000 RPM under worst-case conditions.

It turned out that the real-problem causing the high-RPM misfire was due to a 220 Ohm resistor being installed where there should have been a 10k Ohm resistor. But the above was a good improvement nonetheless.

0.3 Alpha (3rd public release) 2011/04/05

Posted by Michael in 2JZduino.
12 comments

I’m over-due for another release. As mentioned in my post 0.2 Alpha (2nd public release). I’ve been working on replacing the crank sensor intercept with IGT signal intercept. This is now working successfully and is implemented in v0.3 available here…
2JZduino at SourceForge

Here’s a list of significant features new to v0.3…

  • Developer notes are now found in “DevNotes.txt” (they used to be at the top of “IS300_MAPadd.cpp”).
  • Note the inclusion of “IS300_Arduino_EEPROM.exe” in the sourceforge download. This program will talk to and program the EEPROM values into 2JZduino.
  • Igniter control – the code no longer supports interception and reproduction of the crank signal. Instead it intercepts and reproduces the igniter signals: IGT1, IGT2, IGT3. “IGT ON” events are always executed immediately (which charges the igniter circuit). “IGT OFF” events (which marks the firing of the spark) are executed depending on TimingRetardTable[][] values.
  • Fuel Injector pulse-adjustments are now calculated based on the Fuel Injector size stored in EEPROM.
  • Simulated narrowband circuit ground is now connected through PortB0 (Arduino pin 53). The code floats this pin as an input until the LC-1s show they are ready. As an input, the high-impedance drives the stock Toyota ECU into open-loop mode until the LC-1 begins outputting a meaningful signal. When it switches to an input (held low), the ground patch triggers the stock ECU to switch into closed-loop.
  • Improved interrupt efficiency. Igniter interrupts are now always serviced within 40us of their occurrence (20us 95% of the time). Injector interrupts are serviced within 60us of their occurrence.
  • Engine speed now calculated from IGTon signals instead of Crank sensor signals.
  • Datalogger expanded to now include RPM, MAP, Bank1 AFR, Bank2 AFR, and Injector pulse-width data. Datalogger information is now saved in a .csv file by “IS300_Arduino_EEPROM.exe” using the current date/time in the filename.
  • Reduced amount of SRAM used at run-time.
  • Added advanced injector and MAF compensation logic to EEPROM that will scale fuel injectors based on the new vs. stock fuel injector size, new vs. stock MAF sensor intake tube diameter, fuel injector lag, and a DC bias for the MAF signal. *Note: this is experimental and unverified. More details on this in a future post.

Additionally, here’s a summary of the verification testing that I’ve completed since the last release. I’ll be posting some more analytical information on these in the near-term, but for now, here are the results.

  • My 2nd LC-1 wideband has been installed. I’ve now gathered more than 20hours of operation with 2JZduino providing the simulated Narrowband signal for closed-loop fuel control. It’s been successfully fooling the stock ECU into adjusting for an AFR of 15.1 instead of 14.7 while under closed-loop operation.
  • ~4 hours of operation with 2JZduino providing active Injector Scaling (providing leaner fuel injection amounts at wide-open-throttle).
  • Verified that all 10k resistors are indeed 10k resistors… one of the resistors in the circuit for Injector #4 was found to actually be a 220 Ohm resistor. This culprit was understandably causing me all kinds misfire problems for awhile. I’ve learned a good lesson here :)

2ZJDuino v0.1 Shield – Finished Product 2011/01/30

Posted by Michael in 2JZduino.
add a comment

After finally receiving all the parts I’ve found the time to finish building the packaged 2JZDuino v0.1. I recently posted about the custom PCB and packaging design. Below are photos of the actual unit built to that design.

Design-features…

  • 1/4″ pressure line for MAP sensor
  • Mini-Fit Jr. Molex connectors for engine bay connections; LCD, ECU connections, engine connections, power, Wideband sensors, simulated Narrowband signal
  • External ICSP connector for reprogramming
  • Onboard temperature sensor
  • Enclosed in a Bud Industries PI-1908, dimensions: 5.9″ x 3.2″ x 2.4″ (15% smaller than the prototype unit)



  • 2JZduino v0.1 PCBs 2011/01/05

    Posted by Michael in 2JZduino.
    add a comment

    The PCBs made through DorkbotPDX’s PCB Order arrived in the mail today. I’m quite pleased with the quality of the boards. Not that I have that much experience with PCB fabrication, but these exceeded my expectations, and Laen’s prototyping service seems to be the best value ($5/square inch and you get 3 copies). Photo below…

    2JZduino PCBs 2010/12/08

    Posted by Michael in 2JZduino.
    add a comment

    I’m having my first custom PCB made for 2JZduino, using v0.3 of the code (which isn’t available as of this writing). It will support intercepting the IGT igniter signals to control ignition timing (instead of the v0.2 method which controlled ignition by intercepting the crank timing signal). The wiring inside my project box is getting messy enough that I don’t have much trust left in it to be reliable. So these are the first steps toward eventually releasing the Eagle files and a Mouser order BOM for anyone that wants to build one of these for themselves.

    The board is being made through http://www.dorkbotpdx.org.

    The custom board allows for a much cleaner assembly and packaging job. Below are a couple of solid model images of 2JZduino (semi) pupulated and packaged with an Arduino Mega.

    Arduino Buck-boost converter 2010/12/08

    Posted by Michael in 2JZduino.
    5 comments

    [Edit: Previously posted as a “charge pump”, I was corrected by a reader this circuit is actually a buck-boost converter…]

    If you’ve ever needed a negative voltage source for components connected to your Arduino, you might find this post helpful.

    I built a simple buck-boost converter circuit powered by the Timer2 Compare Match Output Unit available on the ATMega 1280 (Arduino Mega). The effort was part of troubleshooting related to the simulated crank sensor output. I thought the stock ECU might need a voltage below ground to prevent noise from triggering extra zero-crossings, but this turned out to not be the problem. So I discarded this as part of my circuit design but it was a breadboard exercise worth sharing.

    First, note that I believe this interferes with the PWM functionality of the Arduino environment. If you use other functions that reference Timer2, this will break them.

    On the Arduino Mega the Compare Match Output Unit can be configured to toggle the value of pin 10 (port B4) at a particular frequency through hardware (i.e consuming zero processor cycles). My charge pump design is configured for an input voltage that switches between 0 & 5V every 0.4ms.

    DDRB = B00010000;  // B4 (pin 10) is an output
    TCCR2A = B01000010; // Toggle OC2A on Compare Match, CTC mode
    TCCR2B = B00000011; // Timer2 prescaler = 1/32
    OCR2A = 0xC8; // Compare Match @ TCNT2 = 200, occurs every 200*32/16MHz = 0.4ms
    

    Below is a schematic for the converter circuit, drawn in LTspice. V1 is Arduino Mega pin 10 (OC2A output). R2 limits the current out of the Arduino. When the output is ON L1 (47mH & 82 Ohm) is energized, and when the output switches OFF, L1 pulls current from C1 (100 uF) through D1 while it discharges. C1 is what stores the negative voltage. R3 is the load consuming this negative voltage. It’s shown as 1k Ohms, but the circuit will maintain -0.5V for R3 values as low as 220 Ohms. For high impedance loads, the circuit will generate approx. -2.7V.

    The voltage at C1 vs. time as simulated in LTspice is shown in the graph below (R3 = 1000 Ohms).

    And that’s it. 4 lines of code and 4 components to generate a negative voltage.