Pages: [1]
Author Topic: Adding FlexFuel to ME7.9.10 step by step...  (Read 1363 times)
woj
Sr. Member
****

Karma: +24/-1
Offline Offline

Posts: 406


« on: June 17, 2018, 02:41:13 AM »

... or how to program your own interrupt driven input Wink

First and foremost - whatever you do with it, you take full responsibility for your doings. I will try to provide help here (hope others will too), but will not provide personal guidance through PMs (unless you pay me Cheesy). Also, there might be errors in the code, I edited it for presentation.

I was thinking for a while now if I should publish this or not, came to the conclusion that there is no such big market for this specific ECU and this feature (well OK, maybe in U.S. there is, there are both Fiats 500 and E85 in the same country). Besides, the way it is presented, it is neither complete nor directly reproducible nor easily transferable to other ME-s, you have to be very inclined to repeat the exercise (and if you can - good for you, I would only appreciate if you acknowledge me if you do it commercially). I will not post ready made patches. So to cut this long intro short, this is mostly for education (something that some of you seem to be craving on this forum). Well, OK, I also want to have some of my code scrutinised, I know it works, but in the process of testing and polishing it I have seen bugs.

I do not intend to give much guidance on the actual E85/E100 tuning, though I will mention some things. I am simply no specialist here, you should ask pros for help. On the other hand, there is lots of misinformation about this out there (including this forum), I will come back to this.

The particular bin I worked with is attached, this is ME7.9.10 software number 1037391689 for Fiat Grande Punto, 120hp t-jet engine. And some links to the ST10 documentation you may find useful (not precisely for the CPU in this particular ECU, but close enough for the purpose at hand):

http://www.keil.com/dd/docs/datashts/infineon/c167cr_um.pdf
http://www.st.com/content/ccc/resource/technical/document/programming_manual/27/c0/48/83/94/9d/4d/45/CD00147146.pdf/files/CD00147146.pdf/jcr:content/translations/en.CD00147146.pdf

Hardware

So, the task is to connect the Continental FlexFuel sensor (picture from Amazon):



to the Bosch ME7.9.10 that runs the Fiat t-jet FIRE engine (picture from ECU Backup site):



And provide a software patch for it. I used the small sensor, part number GM 13577429. When connected according to this scheme:



it will produce a square wave 0-5V signal with the following characteristics, the frequency gives the Ethanol content reading, the pulse width gives the fuel temperature: 

50Hz (20ms period) = 0% Ethanol, 150Hz (6.6(6)ms period) = 100% Ethanol, 180Hz+ -> contaminated fuel
1ms pulse width = -40 °C, 5ms pulse width = 125 °C (some sources say something about 151.25 °C ?)

My first idea was to just go lazy and buy an external control module, like the Zeitronix one (http://www.zeitronix.com/Products/ECA/ECA.shtml), to get 0-5V analog signal and feed that to the ECU through ADC, but then I though it might be worth investigating if it can be connected directly somehow. To save some money and minimise clutter in the engine bay. For that I looked through the disassembly of my bin to see if there are any unused port pins on the ST10 that could be used to hook up an interrupt procedure. There were some potential candidates, but I also wanted something with a connection to the ECU plug. After taking the board under the magnifying glass I found the following connection:



Logic wise, this is exactly what the sensor needs, only there is the additional in-series 100K resistor. I consulted an electronics engineer and was told this should not matter. Out of curiosity I also looked up Fiat t-jet docs I have - the ECU pin is described as "Oil condition sensor input (unused)". OK, let's use it then. Connect the +12V and ground of the FlexFuel sensor to ignition switched power, and the signal line to ECU pin 59 on the small connector (spare ECU plug to get extra connectors might be needed, I did not have one and did a bit of a bodge job there). So, in the end, there was not even any need to open the ECU, just adding one connector to the ECU plug and tapping into some power wires. At the expense of more involved software.

And obviously connect the fuel lines. I connected mine on the return line (mostly for the reason of much lower fuel pressures and less likelihood of having pressure related leaks in my experimental set up). Out there you will find advice to do it on the return line, but also on the feed line (BTW, for high power applications the flow limitation of the sensor might be an issue). From the experience so far I already know that the feed line placement would give more stable readings, on the return line air pockets are caught by the sensor in some operating conditions skewing the readings (can be remedied by software). This is the complete hooked up sensor, minus securing the fuel lines (and fastening the sensor, I still have to do this):



Software

Let's program this thing, the proper Bosch way - let's over-engineer it a bit. I mean, let's measure the CPU clock to get the sensor input precisely (the frequency and pulse width) to one clock tick to get both ethanol content and temperature, do error detection / recovery / reporting, readout smoothing / filtering, etc. The code below has some legacy from my earlier developments, which means things can be simplified and/or omitted, but once they worked I left them as is. In retrospect, to just get this working, things can be probably simplified to a couple of lines of assembly code. But we are here to learn Wink

All addresses in the assembly code below are logical CPU addresses (so not the flash bin addresses like the direct addresses in your map pack, but that should be obvious). The memory map of ME7.9.10 is this. Internal flash is at $0000-$7FFF (as with most (all?) ST10 ME-s), the rest of the flash is mapped to $18000-$CFFFF, out of which $A8000-$BFFFF is the data area ($A0000-$A7FFF also, but it is unused, not sure why). The big RAM is at $F0000. Placing code in some flash areas requires care. For example, $18000-$20000 is the code image for RAM only operation (flashing, recovery boot), but generally can be used if free. The region $90000-$9FFFF, even though tempting (all empty) should be left untouched - this is where the ECU stores the boot code backup during flashing. The DPP registers are initialised as follows: dpp0 is $2A (data area $A8000-$ABFFF), dpp1 is $2B (data area $AC000-$AFFFF), dpp2 is $3C ($F0000-$F3FFF), dpp3 is $3 ($C000-$FFFF). So these areas can be addressed through DPP registers, everything else has to be done with extp/exts.

The code for the sensor will operate on several layers. To start with, we need three things: (1) initialisation procedure that will set up things, reset variables, and enable our interrupt, (2) the interrupt procedure itself for the square wave signal processing with low-level diagnosis, and then (3) the high level diagnosis / application procedure. (2) and (3) will communicate to better discover errors. This is just to get sensor reading, the map modification (fuel, ignition, etc.) is done separately, and so is extending diagnostics interface to report the Ethanol readings.
« Last Edit: June 17, 2018, 02:54:11 AM by woj » Logged
woj
Sr. Member
****

Karma: +24/-1
Offline Offline

Posts: 406


« Reply #1 on: June 17, 2018, 02:45:55 AM »

Finding memory

In terms of free space for variables, code, and data we need the following. A 32 byte register area in IRAM for the new interrupt, an external RAM piece for other variables, flash space for our new procedures, and data area for the calibration data (this does not have to be there by any necessity, but flashing data-only changes later with the right flasher will be quicker). On top of this we will patch a couple of places in the existing code to interface with our new procedures and provide mixture dependant operation of some maps.

The flash space and data space are easily found by simply looking at the bin image. Several possibilities there, I decided to bravely stick code things at the end of $18000-$20000 and data after the factory calibration data:


free_code_area  const   $1F000                 ;; this cannot overrun $1FFF0 (boot fingerprint and checksum)
free_data_area  const   $B7390                 ;; this cannot overrun $BFB78 (checksum data start)
data_area_page  const   free_data_area / $4000 ;; $2D


The RAM things require looking more carefully at the disassembly. Starting at $F3900 there is a chunk of big RAM available until $F3FFC, and then much larger chunk after $F5000. The first one can be directly DPP addressed, so we can save on extp-s and exts-s. Some of these RAM areas are not cleared during ignition off (I believe, did not check this carefully), but that does not matter, we take care of cleaning / resetting ourselves, so:


free_ram_area   const   $F3900
ram_area_page   const   free_ext_ram / $4000    ;; $3C


Then the 32 byte register area. Every interrupt needs its own private register area for r0-r15 (well, not every, but without it things get very hairy). Finding this requires going through the disassembly and looking for "mov $FXXX, r0" instructions (typically followed by "sctx CP, #$FXXX"). This gives you the area where the 32 byte blocks are kept, and then you can identify which 32 blocks there are unused (there is no corresponding write of the quoted form). For our bin that gives at least $F940, $F960, $FAA0, $FAC0 (there are some more). For reasons now unclear to me, I picked the third one:


context_CC9     const   $FAA0



The interrupt set up

So we have the P2.9 pin to service. This corresponds to the Capture-Compare (CC) register number 9 and this is what we have to set up. Pin P2.9 needs to be set up for input, but this is already taken care of in the existing code. We want to trigger the interrupt procedure on both edge changes (remember, we want the frequency and the pulse width of the signal from the sensor) and connect it to a clock with suitable resolution - clock with overflow range larger than our longest possible period (20ms, 50Hz), but small enough range to have precision measurements. The only choice we have for CC9 to have a hardware clock capture is timers T0 and T1 (CAPCOM module 1). And we cannot change the timer setup. On this ECU they are configured in the following way in the T01CON register:

T0: $4A (counter mode, pre-scaler 2)
T1: $42 (duration mode, pre-scaler 2)

We want duration kind, so T1 it is. Pre-scaler is 2, which for the 40MHz clock this ECU has means that the total period of this clock is 52.5ms, resolution (one tick) is 0.8us. Just what we need (if it wasn't like this, stuff would get much harder). The CC9 behaviour is set up in the second nibble of the CCM2 register:


        and     CCM2, #$FF0F    ;; clean up config (can be probably skipped)
        or      CCM2, #$00B0    ;; $B = %1011 - T1 timer, both edges


Another important (very!) thing is the interrupt priority. Too low - it will be pushed away by other interrupts and we will miss edge changes. Too high - it will pre-empt other important interrupts. In fact, with very high priorities the ECU crashes after connecting the sensor. Experimentally, this value seems to work fine (essentially mid range priority):


        mov     CC9IC, #$20


This is also a priority that is not used by any other interrupt (for this you have to scan the disassembly for writes to the other interrupt control registers), this is a good thing to have (though not necessary IIRC). We can then enable the interrupt:


        bset    CC9IC.6


This code so far should go into our initialisation procedure, but after some variables are initialised, so for now just the assembly statements for the interrupt initialisation. 

The interrupt

The interrupt procedure has only one job - to provide raw sensor reading to the higher level procedure. But, since our high level procedure will execute every 100ms (could be more frequent, but really there is no point), and interrupt at least every 20ms, we can already do some signal pre-processing in the interrupt and the rest elsewhere. In the interrupt we will de-bounce the signal, validate edge alternations, validate edge periods, and do modest stepping average of the periods. The higher level procedure will then convert the period recorded in the interrupt, do filtering, and error processing. The interrupt procedure has to be as concise and quick as possible. So, for example, we will not do MDL/MDH operations in the interrupt to calculate the average, but rather do division by 2/4 with shr.

The nice thing about the interrupt procedure is that it has its own private register space (the 32 bytes above). This means that whatever we store in r0 through r15 we can leave it there for the next interrupt round, we can access the registers from the higher level procedure by addressing them explicitly in IRAM (BTW, I do not recall seeing this trick done on Bosch-es, maybe didn't look hard enough, but several times on Marelli systems), and we can have our own private DPP register values to optimise RAM and data access within the interrupt.

The registers will serve the following purposes:

r0 - this has to be the current local stack pointer, even though we do not use the stack
r1 - flags set 1 - status_flags
r7 - flags set 2 - heartbeat_flags
r8 - the clock stamp of the last edge seen (any edge)
r9 - the clock stamp of the last high edge seen (the edge that starts both the pulse width and period)

Otherwise, we can use them freely for temporary storage. r1/r7 separation is one of the legacy things, the flags could be squeezed into one register. In any case, r1/r7 will be IRAM accessed from the higher-level procedure:


status_flags            const   context_CC9 + 2*1  ;; r1
heartbeat_flags         const   context_CC9 + 2*7  ;; r7


Also, the notion of the high/low edge needs care. At first, before I got the actual sensor and simulated it on Arduino instead, I thought the signal is like this:

 ________
|   5V   |_0V_|

where the first chunk is the PW (temperature) and the complete chunk is the period. So all "high" terms in the code refer to the first chunk, the "low" ones to the the second chunk. But, with the actual sensor it is the other way round, freely floating 5V pull-up gets pulled to 0V during the PW chunk. Without rewriting the code I just introduced a calibration flag to account for the other possibility (for example, where the ECU does not allow for direct connection and additional circuitry might be needed).

For calculating the period and the pulse time we use two 12 byte / 6 word arrays:


org     free_ram_area

period_ptr:     db      0, 0            ;; # of correct period (high-to-high) reads in lower byte, wrapping index to reads in higher byte
period_val:     dw      0               ;; average of the last 1-4 reads
                dw      0, 0, 0, 0      ;; last 4 reads
pulse_ptr:      db      0, 0            ;; Same as above but for pulse (high-to-low) reads
pulse_val:      dw      0
                dw      0, 0, 0, 0


Logged
woj
Sr. Member
****

Karma: +24/-1
Offline Offline

Posts: 406


« Reply #2 on: June 17, 2018, 02:47:49 AM »

The interrupt procedure

We are now ready to write the interrupt service procedure. The POFF(X) is X & $3FFF (page offset), DPP(X,Y) is (X*$4000 | Y) (DPP based addressing), the meaning of the bits in r1/r7 is explained as we go:


org     free_code_area

CC9_IR:
        mov     context_CC9, r0
        scxt    CP, #context_CC9                ;; change to our private register area
        scxt    DPP0, #data_area_page           ;; access data area with DPP0
        scxt    DPP2, #ram_area_page            ;; access RAM with DPP2
        bmov    r6.0, P2.9                      ;; copy the pin state before it gets a chance to change
        xorb    rl6, DPP(0, POFF(INV_P29_SIG))  ;; flip the edge meaning (see above)
        mov     r2, CC9                         ;; CC9 holds the time snapshot when the interrupt happened
        jnb     r1.0, CC9_signal                ;; jump if not seen high at least once
        mov     r3, r2
        sub     r3, r8                          ;; r3 holds the time-diff from the last edge / interrupt
        cmp     r3, DPP(0, POFF(DEBOUNCE_TIME))
        jmpr    cc_ULE, CC9_IR_exit             ;; bail out if the interrupt is too quick
        bcmp    r6.0, r1.2                      ;; r1.2 is the previous edge
        jmpr    cc_EQ, CC9_IR_wrong_signal      ;; wrong signal if the edges do not alternate high-low
        bclr    r1.4                            ;; clear the wrong signal flag
CC9_signal:
        mov     r11, r2
        sub     r11, r9                         ;; r11 holds the time to the last high edge (check later if there was one)
        jb      r6.0, signal_high               ;; jump if edge is currently high
        jnb     r1.0, low_done                  ;; we have low edge, if there was no high yet, there is nothing to process
        mov     r13, #DPP(2, POFF(pulse_ptr))
        mov     r14, DPP(0, POFF(PULSE_MIN))
        mov     r15, DPP(0, POFF(PULSE_MAX))
        callr   check_avg_signal                ;; check the pulse (temp.) to be within specified bounds, average the readings
        bmov    r1.5, r1.7                      ;; copy the possible error flag to r1.5 (pulse bound error)
low_done:
        bclr    r1.2                            ;; the last edge was not high (it was low)
        jmpr    cc_UC, CC9_IR_finish
signal_high:
        jnb     r1.0, high_done                 ;; we have high edge, if there was no high yet, there is nothing to process
        mov     r13, #DPP(2, POFF(period_ptr))
        mov     r14, DPP(0, POFF(PERIOD_MIN))
        mov     r15, DPP(0, POFF(PERIOD_MAX))
        callr   check_avg_signal                ;; check the period (E%) to be within specified bounds, average the readings
        bmov    r1.6, r1.7                      ;; copy the possible error flag to r1.6 (period bound error)
        bclr    r1.8                            ;; clear fuel contamination error flag
        jnb     r1.7, high_done                 ;; period in normal bounds
        cmp     r11, DPP(0, POFF(PERIOD_BADF))  ;; Period was outside of normal bounds, check for contaminated fuel frequency
        jmpr    cc_UGT, high_done
        bset    r1.8                            ;; set fuel contamination error flag
high_done:
        mov     r9, r2                          ;; store previous high edge time
        bmov    r1.1, r1.0                      ;; seen two highs if high seen previously (completed one read)
        bset    r1.0                            ;; seen high
        bset    r1.2                            ;; the last edge was high
        jmpr    cc_UC, CC9_IR_finish
CC9_IR_wrong_signal:
        bclr    r1.0                            ;; re-sync of the signal needed - have not seen any highs
        bclr    r1.1                            ;; have not seen any highs
        bset    r1.4                            ;; signal generally wrong
CC9_IR_finish:
        mov     r8, r2                          ;; store previous (any) edge time
        bset    r7.2                            ;; heartbeat for the high level procedure - there is sensor signal
CC9_IR_exit:
        pop     DPP2
        pop     DPP0
        pop     CP
        reti

;; r11 has the pulse / period time to check
;; r14/r15 are the allowed bounds
;; r13 is the pointer to the structure to calculate the stepping average (see above)
;; r1 has the flags
check_avg_signal:
        cmp     r11, r14
        jmpr    cc_ULT, signal_out_of_bounds
        cmp     r11, r15
        jmpr    cc_ULE, signal_in_bounds
signal_out_of_bounds:
        bset    r1.7                            ;; set signal out of bounds flag, copied accordingly to r1.5/r1.6 in CC9_IR
        ret   
signal_in_bounds:
        bclr    r1.7                            ;; clear the signal out of bounds flag
        mov     r14, r13                        ;; maintain the original pointer in r13, use r14 as temporary
        jnb     r1.15, check_bit_14             ;; a request from high-level to clean up the period/pulse reading structure
        bclr    r1.15                           ;; is made through bits 14 and 15 of the status_flag, check them one at a time
        jmpr    cc_UC, reset_reads
check_bit_14:
        jnb     r1.14, no_reset
        bclr    r1.14
reset_reads:                                    ;; reset the reading / averaging structure to all zeros
        mov     r4, #0                          ;; this could be done properly with a loop, but for 6 words
        add     r14, #12                        ;; the amount of total instructions is roughly the same
        mov     [-r14], r4
        mov     [-r14], r4
        mov     [-r14], r4
        mov     [-r14], r4
        mov     [-r14], r4
        mov     [-r14], r4
no_reset:                                       ;; calculate the average of what we already have
        mov     r4, [r14]
        cmpb    rl4, #4
        jmpr    cc_EQ, max_readouts
        addb    rl4, #1
max_readouts:
        movbz   r6, rl4                         ;; get the number of reads so far as a word in r6
        movbz   r5, rh4                         ;; get the index to the read array as a word in r5
        shl     r5, #1
        addb    rh4, #1                         ;; update the index, wrap if necessary (ring buffer)
        cmpb    rh4, #4
        jmpr    cc_NE, no_wrap
        movb    rh4, #0
no_wrap:
        mov     [r14], r4                       ;; update the number of reads and index
        add     r14, #2                         ;; r14 points to the new averaged value to be stored
        add     r13, #4
        add     r13, r5                         ;; r13 points to the new read position in the array
        mov     [r13], r11                      ;; store the new read
        mov     r13, r14
        add     r13, #2                         ;; r13 points to the beginning of the array with 4 reads
        cmp     r6, #1
        jmpr    cc_NE, two_or_more
        mov     [r14], r11                      ;; only one read so far, just use this read
        ret   
two_or_more:
        mov     r3, r13                         ;; use r3 to be able to do post-increment in one instruction
        mov     r15, [r3+]                      ;; we have at least two reads, r15 stores the average of them
        add     r15, [r3+]                      ;; they are now checked to be in bounds, no need to check for overflow (or? Wink)
        shr     r15, #1
        cmp     r6, #2
        jmpr    cc_EQ, store_last               ;; only two reads, r15 is what we need for the current averaged values
        cmp     r6, #4
        jmpr    cc_EQ, four_values
        mov     r6, [r3]                        ;; we have three values, copy the third to r6
        jmpr    cc_UC, add_last
four_values:
        mov     r6, [r3+]                       ;; we have four values, make r6 hold the average of the last two
        add     r6, [r3]
        shr     r6, #1
add_last:
        add     r15, r6                         ;; average r15 (the avg. of the first two) with either the third value
        shr     r15, #1                         ;; or the avg. of the last two
store_last:
        mov     [r14], r15                      ;; store the final result
        ret


Finally, we can install our interrupt in the interrupt vector (in the factory code this is a self loop - the interrupt is never serviced):


org     $0064
CC9INT_handler: jmps    CC9_IR


« Last Edit: June 18, 2018, 01:21:11 AM by woj » Logged
woj
Sr. Member
****

Karma: +24/-1
Offline Offline

Posts: 406


« Reply #3 on: June 17, 2018, 02:48:20 AM »

And of course we need the calibration data that we used in the code:


org     free_data_area

INV_P29_SIG:    db      $01, $00        ;; invert the signal bit, pad
DEBOUNCE_TIME:  dw      312             ;; ignore consecutive pulses within this time range: X * 0.8us = 249.6 us -> 0.25ms

;; Absolute value ranges for error detection on edge duration
PULSE_MIN:      dw      1000            ;; ~<1ms
PULSE_MAX:      dw      6500            ;; ~>5ms
PERIOD_MIN:     dw      8000            ;; ~>150Hz
PERIOD_MAX:     dw      27000           ;; ~<50Hz
PERIOD_BADF:    dw      7000            ;; ~<180Hz


This is first step, it gets us to the point where we have the high-to-high 4-value-averaged time reading stored in period_val, and high-to-low 4-value-averaged time reading stored in pulse_val, effectively raw ethanol content and fuel temperature. And some error flags in r1/status_flags. This now all needs to be converted to more digestible format and dealt with for error reporting, filtering, and the dynamics of the fuel system. And then for map alterations. But for now, I leave you bunch with this to digest and waiting for opinions whether I should continue Wink
Logged
jcsbanks
Full Member
***

Karma: +7/-0
Offline Offline

Posts: 81


« Reply #4 on: June 17, 2018, 03:59:50 AM »

Nice work. There is a shortage of these skills and commercially viable stuff to do on later platforms for sure.
Logged
370rx
Jr. Member
**

Karma: +0/-0
Offline Offline

Posts: 34


« Reply #5 on: October 03, 2018, 06:18:03 AM »

A lot of work has been done!
Logged
woj
Sr. Member
****

Karma: +24/-1
Offline Offline

Posts: 406


« Reply #6 on: October 04, 2018, 02:31:15 PM »

And it continues, I have really shitty time (because of lack of it) to properly calibrate cranking and transients when cold.
Logged
370rx
Jr. Member
**

Karma: +0/-0
Offline Offline

Posts: 34


« Reply #7 on: October 05, 2018, 09:37:07 AM »

when the outside temperature is from 3 to 15 degrees I'm experiencing the same problems as you (you wrote about them in the topic Re: Porsche Cayenne ME7.1 RE85 half warm start problem), and gasoline 95,98. This problem is many of my friends on 1.4 t-jet, but not as obvious as you.
Logged
MyTunes
Full Member
***

Karma: +2/-3
Offline Offline

Posts: 69


« Reply #8 on: November 10, 2018, 09:40:28 PM »

And it continues, I have really shitty time (because of lack of it) to properly calibrate cranking and transients when cold.

Does the MED9 have the ability to do multiple spark like the ME7?

I thought that this had helped some people on E-85 cold startup?
Logged
woj
Sr. Member
****

Karma: +24/-1
Offline Offline

Posts: 406


« Reply #9 on: November 11, 2018, 02:02:10 AM »

Don't understand me wrong - I get a start on the first key all the time, down to 0*C, we did not have colder days than this yet. It's just that it is not as clean and predictable as the gasoline start. Sometimes it turns a lot without catching and then catches on very nicely hitting the target RPM without hesitation (that's probably the sweet spot I want to have), sometimes it catches on much quicker (too quick?), but hesitates and chokes a bit before hitting the target RPM. My ECU has multi-spark enabled by default (if I checked everything right) and I played back and forth with FKSTT, FKSTT reduce factor, small start ignition offsets (though so far one way, I also have to try small retards). The WORST part of all this is that get one (two if I happen to let the car stay for most of the day) try per day, each day different temp. And my AFR controller shuts down during cranking.

Cold transients are a different story, now I have a tank of E15 out of necessity, and I did several cold logs to see how it should be. My E75 pattern seems to match on negative transients, so I should be fine there, yet on acceleration transients I run horribly lean (and get occasional chokes and hesitation) on E75 compared to E15. So either I still need to go much higher with KFBAKL, or start playing with the reduction factor in ZBAKM. But it's all very suspicious, because the log figures tell me I am *pouring* fuel on accelerations rather than injecting it, perhaps I went too far and the lean condition is caused by flooding. And the same problem of one experiment per day.

It seems that now they have introduced a true winter blend of E85 in the country, I judge this from a visibly increased price I see displayed. What exactly is the blend I will get to see once I empty the tank sufficiently.
Logged
Pages: [1]
  Print  
 
Jump to:  

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines Page created in 0.031 seconds with 16 queries. (Pretty URLs adds 0.001s, 0q)