13

How could I go about programming a switch (based on solid-state relay or a triac) that triggers on zero-crossing power?

For these not familiar with the subject: Switch 230V power on, when the sine wave of the power line crosses zero - the result is minimizing the electromagnetic disturbances resulting from rapid spike in current.

Specifically, I'd prefer to move as much into software as possible. The detection circuit consisting of a small transformer, a diode and a couple resistors to keep levels and currents in check provides "1" when the AC input power is in positive half, "0" in negative, attached to an input GPIO pin. The output consists of a few solid state relays and bare essentials to keep them running (pull-ups etc), attached to output GPIO pins.

The problem is timing: with 50Hz AC we get 100 zero-crossings in a second, one half-cycle is 10ms. To get within reasonable distance from zero-crossing to keep said EMI low we shouldn't activate the output more than 10% past (or before) the event of zero-crossing, that means +-1ms tolerance. That doesn't mean 1ms reaction time - we can reasonably expect the next zero-crossing to occur precisely 10ms after the first one, or the fourth - 40ms. It's about granularity - if we allow 20ms for reaction, it must be between 19 and 21ms, not 18 or 22.

How can I implement such a timer - trigger output GPIO either within 1ms since input detects an edge, or within a fixed multiple of 10ms since then - preferably with allowance for some negative bias (say, the transformer and the relay introduce 1.6ms delay; so I want the trigger to go off 8.4+(n*10)ms since the input pulse, that way the bias counteracts the delay introduced by the circuit.) - all of course "on user demand", say, user writes "1" to a /sys/class/... file and on nearest (roughly) opportunity the output goes "on". User writes "0", and when the zero-crossing arrives, specific relay disengages.

I believe this would require writing or hacking a kernel module. Could you point me to what handles the GPIO pins of Raspberry Pi in the kernel, and what kind of timers could I attach to it (unless there are some in place already) to get this kind of functionality?

SF.
  • 920
  • 1
  • 8
  • 21
  • quite interesting project you talk about! Just to give a rough estimate: I first would interface the sine wave from 50Hz original signal via some schmitt trigger logic to GPIO. From there generate an interrupt on the rising or falling edge of the signal.You now are locked on 50Hz AC and could 'predict' when the next zero crossing will occur. Certainly this involves some kernel driver programming. Google is your friend:-) – sparkie Aug 04 '13 at 19:48
  • Take a look at Inmojo's AC dimmer module. I have successfully cloned this for a project using their open source documentation. There is also some Arduino sample cod that may help you through the logic of your code. – Butters Aug 05 '13 at 00:32
  • @Butters: I'm afraid the gap between programming Arduino and programming Linux Kernel module would make the software side useless for me, but the hardware schematics cover 100% of what I need - thanks, you saved quite a bit of my work - the difference between the two projects is only in software (the dimmer must switch the triac cyclically while the switch just powers it on/off once per toggling the switch.) – SF. Aug 05 '13 at 00:43
  • ...100% of the hardware side of course. – SF. Aug 05 '13 at 02:24

1 Answers1

7

You don't need to hack the kernel. You just need to move the process out of the scheduler queue.

    #include<sched.h>

    struct sched_param param;               
    param.sched_priority = sched_get_priority_max(SCHED_FIFO);
    if( sched_setscheduler( 0, SCHED_FIFO, &param ) == -1 )
    {
            perror("sched_setscheduler");
            return -1;
    }

From now on our process receives cat /proc/sys/kernel/sched_rt_runtime_us milliseconds out of each cat /proc/sys/kernel/sched_rt_period_us milliseconds time segment, of uninterrupted execution without risk of being pre-empted during that time (in practice, by default on BerryBoot: 0.95s out of each second.) If you need more, mess with these values, but I don't need more for my purpose here.

I'm using a timer function in milliseconds (that's about the precision I need) based on clock_gettime() to clock my delays.

Calling timer(1) resets it, calling timer(0) returns time since reset.

    #include<time.h>
    typedef unsigned long long ulong64;

    ulong64 timer(unsigned char reset)
    {
            struct timespec t;
            static struct timespec lt={0,0};
            clock_gettime(CLOCK_REALTIME, &t);
            if(reset)
            {
                    lt.tv_sec = t.tv_sec;
                    lt.tv_nsec = t.tv_nsec;
            }

            int r = ((ulong64)(t.tv_sec - lt.tv_sec))*1000 + (t.tv_nsec - lt.tv_nsec)/1000000;

            return r;
    }

You need to link against rt library for this to compile - add -lrt to your gcc command.

Now, for the main loop. I'm using a switch input for "user request" but you can use network, timer or whatever. All you need is to get the boolean value into in.

    while(1)
    {
            //when idle, return a lot of CPU time back to the system. 
            //A call every 100ms is perfectly sufficient for responsive reaction.
            usleep(100000); 

            in  = bcm2835_gpio_lev(SWITCH_PIN);
            out = bcm2835_gpio_lev(TRIAC_PIN);

            if(in==out) continue;   //nothing to do; wait user input, return control to system.

            //The output needs to be changed.
            //First, let's wait for zero-crossing event.
            timer(TIMER_RESET);
            zx = bcm2835_gpio_lev(ZEROXING_PIN);

            //We don't want to freeze the system if the zero-xing input is broken.
            //If we don't get the event within reasonable time, 
            // (like three half-sines of the power; ZEROXING_TIMEOUT = 70)
            // we're going to bail.
            while(timer(TIMER_READ) < ZEROXING_TIMEOUT)
            {
                    if(zx != bcm2835_gpio_lev(ZEROXING_PIN))
                    {
                            //Event detected.                  
                            timer(TIMER_RESET);
                            break;
                    }
            }
            if(timer(TIMER_READ) >= ZEROXING_TIMEOUT) continue;     //Zero-crossing detection is broken, try again soon.

            //Now we are mere milliseconds after zero-crossing event arrived
            // (but it could have taken some time to arrive) so let's wait for the next one, making adjustments for the system delay.
            // This is to be worked out using an oscilloscope and trial and error.
            // In my case BIASED_DELAY = 19.

            while(timer(TIMER_READ)<BIASED_DELAY) ;

            //We can reasonably expect if we perform this right now:
            bcm2835_gpio_set_pud(TRIAC_PIN, in);
            //the signal will reach the output right on time.

            // The 100ms delay on return to start of the loop should be enough 
            // for the signals to stabilize, so no need for extra debouncing.
    }
techraf
  • 4,319
  • 10
  • 31
  • 42
SF.
  • 920
  • 1
  • 8
  • 21
  • Would this work for implementing a pi-controlled dimmer switch for mains a/c? I imagine I would have to 1) change the resolution to something much smaller (instead of every 100ms) and 2) instead of just setting the TRIAC_PIN to in, I would have to set the TRIAC_PIN to 1, wait a determined amount of time (in proportion to desired dimmer level), and then set TRIAC_PIN back to 0. Would this work? – rinogo Aug 21 '13 at 08:55
  • I suppose in the main loop, I'd also want to change the line if(in==out) continue; to if(out==0) continue;, right? Actually, I'm totally new to programming for pi, so maybe that's not necessary - I'm guessing this is all happening synchronously (i.e. we don't have to worry about the main loop being called while nested loops are still executing) – rinogo Aug 21 '13 at 08:59
  • (This is all using the aforementioned Inmojo dimmer module, of course: http://www.inmojo.com/store/inmojo-market/item/digital-ac-dimmer-module-lite-v.2/ ) – rinogo Aug 21 '13 at 09:09
  • 2
    There is a problem with that. For stable system activity you MUST yield control to the system periodically and I really doubt you'd restore it within time as short as (less than) 20ms. So, these yields will result in missed pulses and as result, the bulb blinking. I asked a question about that but got no answers. You might set both sched_rt_runtime_us and sched_rt_period_us to -1 to disable system pre-emption entirely, but if you don't sched_yield() or usleep() at all, that is bound to create problems. – SF. Aug 21 '13 at 09:42
  • 2
    That is: with SCHED_FIFO once you start a time slice, it lasts uninterrupted until you yield voluntarily (or sched_rt_runtime_us is elapsed) but the system does not guarantee when you get that time slice. In my case I noticed in normal operation the time between calls (giving time slices to task) can extend as far as 0.1s with maximum CPU load. Maybe that period can be fine-tuned and forced shorter but I don't know how. – SF. Aug 21 '13 at 09:52
  • Hmm... Good points, and thanks for your feedback! So to make sure I'm understanding right, you're saying that: 1) I won't be able to a) enable the relay, b) wait, and c) disable the relay in less than 20ms (I agree this seems unlikely) AND that 2) sleeping in between enabling and disabling the relay won't work either because I most likely won't regain control for up to 100ms (10Hz, which is far from what I'd need.) Did I understand correctly? – rinogo Aug 21 '13 at 10:31
  • Yes, if your "wait" is done by usleep() or sched_yield() - returning control to the OS, and not like in my example, while(...) ; which just grinds in a busy loop without returning control. If the system is idle you will be receiving control quite often, possibly more than every 10ms, but if system load spikes, that may grow up to 100ms (and I don't know how to force it to be less.) – SF. Aug 21 '13 at 11:04
  • (if you do use the while busy wait, you can even reasonably expect reaction times of order of 100μs or even less! - but if you don't return control to the OS via usleep() or sched_yield() from time to time, and disable the protective mechanism of sched_rt_runtime_us (which would preempt your process eventually) all system demons will be blocked. No WWW, no SSH, no login shells, no cron, nothing! That doesn't work well for an OS with a bunch of essential processes in the userspace.) – SF. Aug 21 '13 at 11:18