Convoy 17mm ramping driver custom firmware notes

In this post I document my journey programming my own firmware onto the Convoy 17mm ramping driver https://www.aliexpress.com/item/32989372464.html. The driver uses linear regulation through a mosfet and opamp, and uses an ATTINY13, which is programmable, and the circuit looks like it could use OTSM to track off time. It is also cheap on AE and so looks like a good board to do custom stuff with. Although I've found some posts mentioning this driver, I have not found anything in depth, so I decided to write this potentially help others.

Updates

2022/6/20 - Added sections 'Let there be moonlight', 'To V or not to V', 'Modding the driver', 'First prototype', 'Final thoughts'

2022/6/1 - Updated current calculations, OTSM discussion

Motivation
Ever since getting my first cheapo zoomie many moons ago, I've wanted to program my own custom driver. Who wants to have to go through an annoying strobe mode all the time when cycling through the modes? Well until now I finally had the right combination of money, electronics experience and an existential crisis to motivate me into doing this.

I firstly had to choose the right driver to program. I didn't want to design my own as that would take too long, it had to be something ready made and easily programmable with the right features. Two candidates that came up were the BLF X6 and the MTN 17DD driver. The MTN one was suitable, however would have cost an arm and a leg to ship down under. I had some concerns about the quality of the BLF X6, and I also wasn't too happy with direct drive, as I would like to know exactly how much current is being used.

I stumbled upon the driver in this post, and upon zooming into the picture, was surprised it housed an ATTINY13. After some hours of research I was able to find some resources on this forum regarding off time calculation, TK's source code repo as well as some circuit diagrams of how a linear driver works, which gave me the confidence to embark on this journey.

Reverse engineering the driver
Once I got the driver, I got to examining the schematic and used a scope to understand how it worked. Below is schematic and pin description of the controller.

PB2 - connected to a resistor divider to measure VCC
PB1 - used a PWM, connected to the opamp to control amount of current flowing through the driver (more details later)
PB0 - connected to end of resistor divider to allow turning off to reduce power consumption during OTSM
PB3 - connected to v+ of the opamp to allow turning on and off the output
PB4 - connected to VCC through a diode to (I believe) allow OTSM to detect when power has been cut

Controlling current through PWM

As shown in the circuit diagram, PB1 is used as a DAC to generate a voltage to the opamp +in. The opamp will then drive the mosfet until the voltage across the sense resistor is the same as the one generated. The voltage divider is to scale the generated voltage to a range suitable to map to the sense resistor value. A higher duty cycle will result in a higher voltage across the sense resistor, thus a higher current.

One interesting thing I discovered here is that the generated voltage is dependend on VCC, so as the battery voltage changes, so does the voltage going to the opamp, supposing the PWM duty cycle remains constant. However observing the stock driver behaviour, this seems to be taken care of by the firmware itself. As I changed the voltage I applied to the driver, the duty cycle of the PWM actually changed. So the firmware actually compensates for this by measuring VCC, and altering the PWM signal. Realising this, I did some maths as listed in the below scrappy code notes, which should explain how the duty cycles can be calculated.

/**
* 
* PIN 6 +
* |
* 261k
* +--------- OPAMP +in
* 5.6k
* |
* GND
* 
* OPAMP -in is connected to 0.005 ohm sense resistor.
* 
* PWM is used to control voltage of the sense resistor.
* Frequency is 1.2khz
* 
* The maximum voltage to the opamp, when PWM is fully turned on, is
* 
* = VCC * 5.6/(261+5.6)
* = VCC * 0.021005251
* 
* ADC voltage divider top value maps to 1.1 * (22 + 5.6)/5.6 ~ 5.5v
* VCC = (ADC/1023) * 1.1 * (22 + 5.6)/5.6
* = ADC * 1.1 * (27.6/(5.6*1023))
* = ADC * 1.1 * 0.004817763
* 
* If intended current is I, then we need the sense resistor voltage to be
* = I*0.005
* 
* * So, the PWM value is
* ((PWM+1)/256) * VCC * 0.021005251 = I*0.005
* PWM = (I*0.005*256)/(VCC*0.021005251) - 1
* = (I*0.005*256)/(ADC*1.1*0.004817763*0.021005251) - 1
* = (I/(1.1*ADC))*(0.005*256)/(0.004817763*0.021005251) - 1
* = (I/(1.1*ADC))*12648.431183657 - 1
* = (I/ADC)*(12648.431183657/1.1) - 1
* 
* The 1.1 bandgap voltage value left as is to allow for it to be calibrated
* 
* e.g. when VCC = 4.2, then the ADC reading is 792
* If we want a current of 5A, then the PWM duty cycle out of 255 is
* = (5/792)*(12648.431183657/1.1) - 1
* = 71
* 
* Alternatively we may alter the top value of the PWM for finer control of lower voltages
* ((PWM+1)/(TOP+1)) * VCC * 0.021084337 = I*0.005
* TOP = ((PWM+1)*VCC*0.021005251)/(I*0.005) - 1
* = ((PWM+1)*ADC*1.1*0.004817763*0.021005251)/(I*0.005) - 1
* = (((PWM+1)*ADC*1.1)/I)*(0.004817763*0.021005251)/0.005 - 1
* = (((PWM+1)*ADC*1.1)/I)*0.020239664 - 1
* 
* e.g. when VCC = 4.2, then the ADC reading is 792
* If we want a current of 0.1A, then setting OCR0B = 0 we get TOP
* = ((1*792*1.1)/0.1)*0.020239664 - 1
* = 175
* 
*/

And so we arrive at these functions which can be used to set the current level

// 50mA per unit
// e.g. 200 = 10.0A
// cannot be less than 2 e.g. 100mA
void setCurrentAccurate(uint8_t current)
{
uint16_t adc = measureVCCPin(100);

// PWM = (I/ADC)(12648.431183657/1.1) - 1
// lose 1 bit of precision to allow using 16bit math
// = (I
((12648.431183657/1.1) >> 1))/(ADC >> 1) - 1
// = (i*((12648.431183657/22) >> 1))/(ADC >> 1) - 1
// where i is 1/20 of an amp
uint8_t duty = current * (574 >> 1)/(adc >> 1) - 1;

// TOP = (((PWM+1)ADC1.1)/I)0.020239664 - 1
// = (((PWM+1)ADC1.1
0.020239664)/I) - 1
// = (((PWM+1)ADC1.10.02023966420)/i) - 1
// where i is 1/20 of an amp
uint8_t top = ((duty+1)adc4L)/(current*9) - 1;

OCR0A = top;
OCR0B = duty;
}

// 50mA per unit
// e.g. 200 = 10.0A
// cannot be less than 2 e.g. 100mA
void setCurrentByDuty(uint8_t current)
{
// PWM = (I/ADC)(12648.431183657/1.1) - 1
// lose 1 bit of precision to allow using 16bit math
// = (I
((12648.431183657/1.1) >> 1))/(ADC >> 1) - 1
// = (i*((12648.431183657/22) >> 1))/(ADC >> 1) - 1
// where i is 1/20 of an amp
uint8_t duty = current * (574 >> 1)/(measureVCCPin(100) >> 1) - 1;

OCR0B = duty; // PB1, OCR0B = 255 means fully on
OCR0A = 255;
}

// 50mA per unit
// e.g. 200 = 10.0A
// cannot be less than 2 e.g. 100mA
void setCurrentLow(uint8_t current)
{
// TOP = (((PWM+1)ADC1.1)/I)0.020239664 - 1
// = (((PWM+1)ADC1.1
0.020239664)/I) - 1
// = (((PWM+1)ADC1.10.02023966420)/i) - 1
// where i is 1/20 of an amp
uint8_t top = (measureVCCPin(100)4)/(current9) - 1;

OCR0B = 0;
OCR0A = top;
}

// cannot be less that 10 e.g. 500mA
void setCurrentMed(uint8_t current)
{
uint8_t top = (measureVCCPin(100)8)/(current3) - 1;

OCR0B = 5;
OCR0A = top;
}

setCurrentAccurate is the rolls royce of the methods. It will first pick the closest PWM on time value to match the given current e.g. out of 255 steps, how much on time will give the required current. It will then use that on time to trim the off time to make the duty cycle more accurate e.g. with an on time of 10 steps, how much off time will give the required current e.g. 250. If only the on time is used to vary the current, then this means we jump in 0.06A steps, which may be bad especially at low light levels. This calculation however requires 32bit maths and will take up too much space on the ATTINY13.

The other methods are optimisations to save space. For example, for setCurrentLow, we fix the on time to 0 (on an ATTINY13 this results in a 1/256 PWM). We then vary the off time to achieve various current levels. This is much more accurate than varying simply the on time, e.g. the brightness between 1/256 and 2/256 is much bigger than 1/256 and 1/255.

The initial tests up to 500ma on a spare LED were promising, with the current regulation approximately accurate to 10-15%, without any calibration whatsoever. Looks like the calculations were promising so far!

OTSM click detection

OTSM was done similarly to the otsm example from FlashyMike, using PB4 to detect power off. The onboard cap on the driver seems to be sufficient to allow this to function for my 300ms of off time. At lower voltage levels this does get a bit tight however.

It was during this time I realised my idea of OTSM did not work. The input to PB4 from VCC is simply a floating pin when the power cuts off, so the controller does not detect power off. My work around is to solder a 100k resistor from PB4 to ground. This was about the highest value before power off couldn't be detected again using higher resistor values. This is seen in the title image.

Unfortunately I had already flashed over the original firmware of the driver at this point so cannot do more tests. If anyone knows how this pin is actually used please comment, or I'll have to wait for some more drivers to come :D.

Programming the driver

I bought a SOIC8 test clip to do the programming, but was dismayed when due to the design of the driver components, the clip does not fit onto the controller well. I cut some parts of the clip off to try to fit it in, and eventually succeeded, however the clip does not clip securely anymore, and only works when I hold it in the right position. This is ok for single programs, but for development I will have to rely on my breadboard with through hole components.

The ATTINY13 also only has 1kb ram, and I am only able to fit my crappy unoptimised OTSM + mode switching + hidden turbo + voltage readout code onto the program.

Let there be moonlight

One feature that interested me is having an ultra low power moonlight/firefly mode which can be left on indefinitely. Who needs a lighted tailcap? The idea is to set a current level to the opamp, then PWM the opamp V+.

One worry I had was that the components would need a start up time. You can find similar discussions with how moonlight mode is implemented using an AMC7135, which has a start up time of 2us, therefore limiting the PWM frequency. Through some experimenting, I found that the start up time on this driver was... 100s of us! In the below picture is the scope of the opamp output when fed a PWM signal of 1kHz. Although the opamp started up quite quickly, the voltage it starts at is not enough to turn on the mosfet. It took around 600us before the mosfet started to glow. Once it started glowing, decreasing the off time of the PWM increased the brightness. Well it actually works! The 1kHz PWM frequency was not visible, there was no visible flickering, and by controlling the off time I could get the LED to light up the most miniscule amount, as in the second picture.

Now, the issue was how to make the brightness consistent across voltage levels. As the voltage level decreased, so did the output from the light if the same PWM duty cycle was maintained. There was no magic formula as in the current level in the previous section. I decided to use a calibration method where the off time was chosen at 2 different voltage levels, and then the off time is linearly interpolated between them. This works OK for low levels of light, however for brighter levels, the light drops brightness as voltage decreases. I am yet to find a good way to scale the off time, but I'm happy with how it works for the firefly levels.

At the firefly level as shown in the picture, the current draw of the LED was 0.05mA. Couple that with a 0.4mA processor current consumption, this means I'd be able to leave the light on this mode for half a year.

To V or not to V

The ATTINY85 comes in a V version, which means it is guaranteed to work up down to 1.8v. The regular version is only guaranteed for 2.7v. I initially did not consider this an issue as I had been testing with my regular ones down to 1.8v with no issue. However the datasheet mentions that working at these lower levels could lead to corruption of the flash. If by chance at low voltage levels, the currently running instruction gets corrupted into one that writes to flash, then it could mean the whole program could be overwritten. I was dismayed at this, as I didn't want to risk this happening to a flashlight, which should be reliable if needed in an emergency situation. I had already ordered a few regular versions, which had gotten very expensive. Well, can I still use the regular ones?

Setting brown out detection will prevent any corruption from occurring, however the main concern is it not allowing running the battery down to it's full capacity. When using the regular chips, we should set the brown out detection to 2.7v. The datasheet says that the worst case BOD level is actually 2.9v, so in the worst case, the flashlight will stop working when the chip goes below 2.9v. The driver puts a diode between the VCC and the chip, which has a voltage drop of around .25v, so the actual battery level will be 3.15v. In addition, we need some extra voltage to allow OTSM to work, so adding 0.2v, we get a minimum battery level of 3.35v. If we take the typical 2.7v BOD, then it's 3.15v, which is not tea bag, and can be considered a conservative LVP level.

So one thing that the firmware could do to help is to go into an LVP state at lower voltages where click detection is unreliable, so the flashlight remains usable down to the BOD level.

I decided to order one ATTINY85V version, but also continue with the above assumptions as a reasonable trade off.

Modding the driver!

The day finally came when the ATTINYs arrived in the mail! Well off go the ATTINY13. I did not realise that the 85 comes in a bigger package than the 13. It is a 200mil SOP8 package, vs 150mil of the 13. Some slight bending of the pins allowed it to fit onto the board. There is an SMD resistor underneath the ATTINY to pull the OTSM pin to ground as discussed in the previous section. Tightest solder job of my life!

First prototype

The firmware still needs polish, but I was itching to try it out. And I'd only know how I'd like the driver to behave by testing it out. Into an S2+ the driver goes! I was happy to see that most things worked as expected. This was the first time the current regulation was tested at higher than 500mA, and it seemed to be working without a hitch! I took it out for a night walk and was happy it behaved as expected.

Final thoughts

Overall, the driver has indeed met my expectations, and I'm quite happy with what I could do with the prototype. I was able to add mode switching, double click turbo, moonlight, voltage/temperature readout and configuration modes to the driver. The driver also ran way hotter than my stock 5A S2+ driver, so I assume it was able to pull more current than it, however at that level the brightness increase is hard to spot.

After the night walk and using the light, I realised that soft pressing a clicky is not so reliable (compared with say e-switch), and so although an extensive UI could be implemented, it would not be too reliable to use. That's not to say I wasn't able to configure the flashlight using multiple presses, but sometimes a double click might take too long because the tactile pressure applied by the finger can be inconsistent. So I would probably stick with a simpler normal everyday UI, but have the other cool modes available deeper in the UI.

I've not discussed the firmware much, but I have written it in C++, using ideas from reading posts here and much thinking. The idea is a hierarchical state machine using virtual functions, with an ECS like game loop. It took a few iterations before I arrived at this state. One issue I ran into was running out of memory on the ATTINY due to vtables taking up too much of the measly 512bytes of RAM. Enjoyment of the coding process is also an important part of the project. I found the class structure was able to organise the different states of the flashlight quite well. Luckily I was able to remove some unnecessary virtual methods to free up enough RAM.

I will continue to refine the driver to my liking, hope you've enjoyed the read.

I’m really looking forward for the update. Good work with reverse engineering :beer:

Are you going to mod some firmware for the driver with attiny13 or are you focused on attiny85 version?

This is great. I’m looking forward to future developments.

Thanks for the interest, I’ll keep updating as I go, however I think I’ve documented most of the fun stuff. Now I just have to wait a few weeks until the rest of the parts arrive, during which I can code up the firmware.

Yeah I was able to put something very minimal on the tiny13. It would be nice to use it as then I wouldn’t have to mod the driver and replace SMD parts on it, but it’s not enough space for what I’d like.

I’m imagining features like ultra low always on firefly mode for this driver, e.g. set current to 100ma and PWM the opamp on and off. Running the tiny85 at 250khz I was able to measure 0.4mA current consumption, we’ll see if it actually works on the driver in practice.

This is great. I have a SONIX programmer, and eventually intend to understand the 12-modes driver, to see if an open firmware can be implemented on it. I’ll bet the control systems are similar.

This should be expected when the diode (D2) has the direction as shown in your circuit diagram. Are your sure it does really have this orientation? If it were the other way around the original firmware could have used the internal pullup at the input pin.

At least the LED (D3) shows the wrong direction - it won’t lit up.

Anyway - I connect the input pin for power down recognition directly with battery plus with nothing in between.

I double checked the driver with a multimeter and the diode direction is as I have listed in the diagram, going into the pin. This also proves why my initial prototype worked without a pull up, as VCC is what’s keeping the pin high. Yeah the LED is the wrong way around :person_facepalming:

I suspect the floating pin problem is exacerbated by my test setup where I have long leads from VCC causing noise onto the PB4. Perhaps in practice as you have done with your drivers, if the driver is sealed inside a host (maybe it acts like a faraday cage), and the traces are short then this isn’t much of an issue. https://www.avrfreaks.net/forum/unconnected-pins-usually-dont-float This post suggests pins bias towards ground, but any long traces will cause noise to trigger pin change. Given what I have experienced I’d feel safer having a pull down to ensure the pin state is known.