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

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,353
  • 10
  • 32
  • 43
SF.
  • 920
  • 1
  • 8
  • 21