Giter VIP home page Giter VIP logo

triacdimmer's Introduction

TriacDimmer

The high-performance arduino triac dimming library.

This library was designed to perform phase-control dimming control on a triac dimming circuit, leveraging the ATmega328p's built-in timer peripheral to perform all time-critical functionality directly in hardware, without the need to spend CPU time on an expensive control loop.

Note that this library is intended to control mains AC power. Make sure you understand the risks and take appropriate precautions before working with mains AC.

The phase offsets are calculated based on the measured mains frequency, so this code will work regardless of 50/60Hz or any other frequency. This includes correcting for any inaccuracies in the arduino's oscillator or the mains frequency.

This library was developed specifically for the Krida 2 CH Dimmer (amazon, alibaba, inmojo), and has been tested to work with the RobotDyn AC Dimmer (robotdyn), and should work fine with other phase-control dimming circuits that output a positive edge on their sync signal.

See the example for an example of how to use the library. The library methods themselves are documented in the library header.

This library requires the use of certain pins. Pin 8 must be used as the sync input, and pins 9 and 10 are the only pins that can be used as channel outputs. This library will not work on any other pins, period.

fritzing diagram

Flickering, and How to Fix It

If you experience issues with flickering, here are a few things you might want to consider/try:

Frequency Drift Calibration

This library automatically recalibrates against frequency drift every time setBrightness is called, and the design decision to have the library do it this way has implications for how the library needs to be used. (A better design might make this 100% automatic in the future, though.)

In order for the frequency drift calibration to work as expected, the library needs to observe a full waveform period between the initial begin and a setBrightness call. Without this, the library will assume a default calibration, but this default calibration is not going to be accurate, and is entirely wrong for any situation other than a 16MHz arduino controlling 60Hz power.

Ensuring you have a valid drift calibration could mean something as simplistic as sleeping for 20ms between TriacDimmer::begin and TriacDimmer::setBrightness, but in practice it's easier to just have one of your main loop tasks be calling setBrightness -- that way it will continuously compensate for temperature changes too.

Signal Noise

When working with power electronics, it always pays to be paranoid about signal noise.

This library automatically enables the integrated noise-filtering function of the hardware it uses to help reduce the impact of noise on the sync input -- but this can only do so much.

More filtering can be added by wiring a small capacitor (e.g. 1nF) between the sync signal and ground, close to the arduino board; or an even more advanced low-pass RC filter for those willing to do the math.

In addition, you should also look at ways to reduce the nose at its source instead of just filtering it out after the fact.

You may not need to be quite as paranoid as doing everything described here -- in fact, you most definitely should not do the last item -- but if it's not working yet...

  • Keep low voltage and high voltage wires as separate as you can manage, and make sure they are not parallel with each other.
  • Minimize loop area. Run the neutral wire for the circut backwards along the exact same path and as close as your insulation clearances allow to the hot wire, so that any magnetic fields from current in one wire are cancelled out by the return current in the other.
  • Add shielding. If low voltage and high voltage ever do need to be close or parallel, ensure at least one of the two is covered as much as you can manage with some sort of grounded metal -- whether that's a metal box around one of the two sections, a bot of copper foil tape wrapped around a bundle of wires, or whatever else you need.
  • Realize that single-ended signals are a lie we tell beginners and car mechanics -- everything is differential, how differential jut depends on frequency. Run a twisted pair for each signal and use the other conductor in each pair as a dedicated signal return connected to the closest ground at each end.
  • Abandon all hope and confront your imposter syndrome by adding "real" differential transcievers.

Tune the Pulse Generation Parameters

There are a handful of parameters you can pass to begin that can be adjusted depending on what sort of flickering you have. By default, with no arguments the library uses these values as defaults:

TriacDimmer::begin(pulse_length = 20, min_trigger = 2000, on_thresh = 2, off_thresh = 0.01)

First off, if you experience flickering regardless of the brightness value you set, increase pulse_length from the default 20 to a larger value like 50 or 100 until TriacDimmer::setBrightness(pin, 0.5); results in a stable glow.

Once it's stable at 0.5, set the brightness 1.0 (TriacDimmer::setBrightness(pin, 1.0);) and check for flickering. There shouldn't be any, but if there is you can increase min_trigger from the default 2000 to perhaps 3000 or 4000 until the flickering stops. If you're still experiencing flickering no matter how large min_trigger is, you can also try setting on_thresh to below the highest brightness level that causes flickering. This shouldn't normally be necessary though, as adjusting min_trigger should normally be enough.

The last step is to figure out the lowest brightness value can sustain without flickering. By default the library is set to cut off completely for brightness values smaller than 0.01, but if you still see flickering at 0.015 or 0.02 you can try setting off_thresh to a value that's larger than that.

Open an Issue

If you've tried some or all of these things and still get flickering, consider opening an issue. Make sure to include as much information about your setup as you can, including the specific dimmer board you're using and the code you are using to drive it. If you have access to an oscilliscope, please include screenshots (or if you must, phone pictures of the screen), showing the mains voltage input, the mains voltage output, and the arduino's sync input and trigger output pins at the arduino. (If you only have two channels rather than four, the most important two signals to examine are the sync input and the mains voltage output.)

Theory of Operation

For those having trouble with this library, understanding the underlying theory of operation may be helpful. For others, if you're looking to get into programming microcontrollers at the register level, this library might actually be a good example codebase to study.

Either way, I'd highly recommend looking through the Atmega328p datasheet, specifically chapter 15: 16-bit Timer/Counter1 with PWM. This library uses most of the features of this pherhipheral, and that datasheet is the primary source of truth for how it works.

The library uses Timer/Counter 1 in free-running aka "Normal" mode, where the counter is allowed to count up continuously and overflow from 0xFFFF back to 0x0000. It's set to run from the system clock, with a divide-by-8 prescaler that ensures that the counter's 16 bits are enough to represent the period between zero-crossings and avoid aliasing between overflows.

With the most common 328p Arduino boards with external 16 MHz crystals, (e.g. an Uno, Micro, Nano, Leonardo), each count represents approximately 0.5us, and the timer overflows back to 0 approximately every 33ms. On other boards that use the 328p's own internal 8 MHz oscillator, each count represents a nominal 1us, for an overflow about every 65ms -- but operating under conditions that put it closer to the edges of the ±14% tolerance, each count could represent anything from 0.86us to 1.14us, and the overflows could be anywhere from 57ms to 76ms. Importantly, it doesn't actually matter how long the timer counts are -- the "timer count" is the fundamental unit of time being measured by this timer for the purposes of this library, and there's no actual need to relate that unit to more conventional units of time like milliseconds or microseconds, as long as the events being measured are more frequent than the overflow.

For comparison, on 50Hz power there are 100 zero-crossings per second, so the time between those events is 10ms; whereas on 60Hz power there are 120 so the time between those events is 8.3ms. Either way, the time between events is enough smaller than the overflow period to avoid aliasing.

The core functionality of this library revolves around the interplay between the counter's Input Capture unit and the Output Compare units when used in free-running normal mode.

The Input Capture unit is a piece of hardware that, when triggered by an external pin change, instantly copies the current value of the counter to its ICR1 register. The Output Compare units have a similar function, but in reverse; they continuously wait and check for the counter to reach the value programmed into their OCR1 register (OCR1A for unit A, OCR1B1 for unit B), and then instantly change the value of the associated pin.

The value captured by the input capture unit acts as something of a timestamp; it can be compared to work out the time between events by subtracting a previous timestamp, or used to calculate a value to set the output compare register to to generate an output after a precise duration.

Both of those concepts -- comparing capture timestamps and computing output timestamps -- are used by this library. Probably the easiest-to-follow examples of this are in the TIMER1_CAPT interrupt service routine.

The comparison between captured timestamps happens on lines 163-164:

	TriacDimmer::detail::period = ICR1 - last_icr;
	last_icr = ICR1;

TriacDimmer::detail::period is a variable that the library uses to store the measured half-wave period, in timer-count units, and communicate that value from the ISR context to what I'll call the "userland" half of the library.

And calculating and setting the output timestamps happens a few lines earlier, lines 152-153:

	OCR1A = ICR1 + TriacDimmer::detail::ch_A_up;
	OCR1B = ICR1 + TriacDimmer::detail::ch_B_up;

TriacDimmer::detail::ch_A_up and ch_B_up are variables that the library uses to store how long the pause between the zero crossing and start of trigger pulse should be, in timer-count units, and communicate that value from the "userland" half of the library to the ISR. (The library also has TriacDimmer::detail:ch_A_dn and ch_B_dn that are used in the output compare service routines, to set up the timer to end the trigger pulse at the appropriate time once the pulse has started; this works almost the same but with some further complications around needing to buffer this value to avoid a data race.)

The "userland" end of that communication can be seen in TriacDimmer::detail::setChannelA and it's siblings. The code there might not be super readable, due to the ATOMIC_BLOCK parts needed to avoid a data race and the logic for ensuring the pulses always end correctly, but if you were to strip out all of that, fundamentally what it's doing is this:

void TriacDimmer::detail::setChannelA(float value){
	TriacDimmer::detail::ch_A_up = TriacDimmer::detail::period * value;
}

This operation is the key to the library's ability to compensate for different mains and clock frequencies. And the use of the measured period ensures that the pulse will be proportional to the actual mains frequency, however many timer-counts its period happens to be. And because both the period and pulse offset are measured in timer-counts, variation in the absolute size of each timer-count cancels out.

This operation is also the reason for the split between the ISR and "userland" parts of the library. One thing to note, is that the 328p does not have hardware floating point, so an arithmetic operation like this internally involves making a subroutine call into a software floating point routine. This wouldn't necessarily be too expensive to do in an ISR, but in general it's good practice to do as little work as possible in ISRs, and in specific the library's margins for how close to the start/end of a cycle and how short of a pulse it can generate are directly affected by how long its ISRs take to run. So this arithmetic is done in the "userland" portion.

Because of this, though, this recalibration doesn't happen automatically the way the interrupt-handling and pulse-scheduling parts do; in order to get the library to re-calibrate against any drift in the system clock frequency or mains frequency, these "userland" routines need to be called again, and in theory called regularly in order to keep recalibrating.

In the examples, the main code loops continuously, calling setBrightness() regularly (about every 20ms in basic_example.ino, and continuously as it's polling the analog input in potentiometer.ino). Though it doesn't necessarily need to be called continuously like that, it does need to be called at some point after the system has managed to capture and measure a pair of actual zero-crossing events -- i.e. a couple dozen milliseconds after the initial TriacDimmer::begin() call.

triacdimmer's People

Contributors

ajmansfield avatar nsummy avatar per1234 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

triacdimmer's Issues

Correct For Drift Automatically in ISR

Implement an operating mode that uses a fixed-point binary fraction representation of the requested brightness to compute the pulse offsets from the waveform period every cycle.

Based on a design possibility uncovered in a discussion of what turned out to be an unrelated issue here: #23 (comment)_

Allow using only a single channel.

Split from #11.

Credit to @JAndrassy for his suggestion:

Additionally I use pin 10 as slave select for Ethernet shield and your library doesn't have an option to use only one pin of Timer1.

The library ought to be able to be configured to use only one of the two channels and leave the other free for other uses.

Use with potentiometer?

Hi, I am attempting to use this with a potentiometer and am not able to get it to work at all. I realize its looking for a floating point value between 0 and 1. Is there a good way to set this up? Or a simple way to modify the source to use an integer input? Unfortunately I am still learning to code, so while I understand the problem, I am not sure of the solution. Thanks :)

Warning in pin comparison

/home/anson/Arduino/libraries/TriacDimmer/src/TriacDimmer.cpp:48:10: warning: suggest parentheses around comparison in operand of '&' [-Wparentheses]
  if (pin & 0x01 == 0x01){ // if (pin == 9){

Investigate waveform asymmetry.

In cases where either:

  • the mains voltage source used for an application has a significant DC component,
  • the zero-crossing detector is biased (i.e. emits its pulse earlier on a positive-to-negative transition than a negative-to-positive transition), OR
  • the triac and/or triac driver circuitry used to switch the output is biased (i.e. the triac switches on more quickly in one direction than in the other)

It's possible that better performance and higher accuracy could be achieved by treating alternating half-waves independently from a timing perspective.

As an extreme example, take this:
waveform

A reasonably simple solution would be to split all current timing variables into two separate variables, corresponding to the separate positive and negative periods. This would involve some overhead from needing to track the alternating phase and index.
This would eliminate any asymmetry caused by a DC offset.

The other two asymmetry modes would require adding an additional half-wave offset configuration parameter that describes the asymmetry caused by the zero crossing detector and triac characteristics.
We would also need some way to synchronize with the correct half of the phase to ensure the offset is applied in the correct direction on every power-up. Assuming some of the asymmetry is caused by the zero-crossing detector, this could potentially be determined purely based on which half of the phase is longer, but if that is not the case this will require additional input circuitry and another input pin to read the phase directly.

Note that at present these issues are purely conjectural possibilities. Before trying to remedy this hypothetical problem, we need to take actual measurements on actual hardware to assess how significant these actually are.
We also need to assess whether this edge case is significant enough to be worth the performance penalty a fix would incur against the general case.

wavedrom source for image:

{signal: [
  {name: 'mains', wave: 'du.d...u.d.'},
  {name: 'zero',  wave: 'lPlPl..PlPl',
                  node: '.A.C...E...'},
  {               node: '.B.D...F...'},
  {               node: '...IH..L.K.'},
  {name: 'triac', wave: 'l...Pl...Pl',
                  node: '....GM..OJ.'},
  {               node: '.....N..P..'},
],
edge: [
  'A-B', 'C-D', 'B<->D t1',
  'C-D', 'E-F', 'D<->F t2',
  'C-I', 'G-H', 'I<->H 50% t1',
  'E-L', 'J-K', 'L<->K 50% t2',
  'N->M', 'N should be here',
  'P->O', 'P should be here'
],
config: {
  hscale: 2
}}

Support for more chips/boards.

This library at present only supports 16MHz ATMega 328p boards like the arduino uno, and only on timer/counter 1, but in theory the exact same code could support a much wider range of cpus that have the same peripheral available.

Use with boards with >2 channels

I can see that there are a limitation for controlling only 2 channels.

Is it possible to adapt this library to control boards with >2 channels, for example Krida 8CH Dimmer board?

If yes, could someone point me to the direction what needs to be updated in code and what are the challenges?

external interrupt for zero crossing?

What is the advantage of using timer capture over external interrupt? I made this and then I discovered your library and I try to understand.

const byte TRIAC_PIN = 9;
const byte ZC_EI_PIN = 2;

unsigned long topMicroseconds = 9700; // 10000 micros is between zero crossings
int prescaler = 8;
byte prescalerBits = _BV(CS11); // /8

void zeroCrossing() {
  TCNT1 = 0; // reset the timer counter
}

void setup() {
  Serial.begin(115200);
  Serial.println("START");

  attachInterrupt(digitalPinToInterrupt(ZC_EI_PIN), zeroCrossing, RISING);

  pinMode(TRIAC_PIN, OUTPUT);
  uint32_t topPeriod = ((F_CPU / 1000000)* topMicroseconds) / prescaler ;
  ICR1 = topPeriod;
  OCR1A = topPeriod + 1;
  TCCR1A = _BV(WGM11) | _BV(COM1A0) | _BV(COM1A1);
  TCCR1B = _BV(WGM13) | _BV(WGM12) | prescalerBits;
}

void loop() {
  if (Serial.available()) {
    unsigned long microseconds = Serial.parseInt();
    Serial.find("\n");
    uint32_t period = ((F_CPU / 1000000)* microseconds) / prescaler ;
    OCR1A = period;
  }
}

I use Robotdyn Dimmer

Unstable operation using the motor instead of the bulb

Hi,
I have a problem controlling an AC motor using this library. Generally, the engine changes speed smoothly and works correctly both at minimum and extreme setBrightness values. But sometimes (I don't know how to write it exactly) it seems to lose synchronization and starts "firing", as when the engine is burned out or the brushes are running out. Sometimes it can fire once every few minutes, and sometimes several times in a row. I hope you know what I mean.
I tried using different values in the configuration (pulse_lenght, min_trigger, etc.) but it did not solve my problems. The engine is definitely functional because I tried it on several others and the effect was repeated. Maybe there is something that can be changed in the code to prevent this effect from occurring?

doesn't work with Robotdyn AC Dimmer

With Robotdyn AC Dimmer (schematics) and basic_example.ino the light is full on and sometimes flickers.
I test with Arduino Nano.
I added some debug prints in ISR(TIMER1_CAPT_vect) after last_icr = ICR1:

  Serial.print(micros());
  Serial.print("\t");
  Serial.print(last_icr);
  Serial.print("\t");
  Serial.println(TriacDimmer::detail::period);

output is

1408	2793	2793
11420	22824	20031
12168	22824	0
21440	42855	20031
31460	62905	20050
32208	62905	0
40448	17381	20012
50464	37423	20042
51212	37423	0
60476	57439	20016
70500	11952	20049
80512	31984	20032
81260	31984	0
90540	52027	20043

sorry I rewrote this issue twice until I debugged the problem.
the problem is, there are some phantom zero crossings resulting in period 0 and brightness is calculated with this value

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.