Fir-tree, light up! Part 2: C software, GPIO and software PWM

    Hello, Habr!

    Last time I wrote about how you can easily connect our nanocomputer to something executive (Christmas tree garland, for example) and deploy an environment for building C / C ++ programs for it. Today is the second part on how to write a C program for OpenWRT and, accordingly, Black Swift.

    1. Garland, Black Swift connection and build environment for OpenWRT in C / C ++
    2. C control program and direct and fast GPIO operation
    3. Web UI and Android App


    I note two points. Firstly, I will not talk about the role of the main function and the #include directive - as I said earlier, now I write based on people who, in principle, are familiar with programming, but who do not know which side to approach such a thing as a computer for embedded use. More precisely, today's text will be more likely for those who have already approached - so I will focus on a couple of interesting points, such as working with logical input-output lines (GPIO) and microsecond times.



    Secondly, of course, writing in C is not necessary. There are Perl, Python, and even Node.js under OpenWRT - you do not need them, obviously, no environment for building software, just upload @ run.

    Quick and easy work with GPIO



    We have a Christmas tree garland in our hands, controlled by four half-bridge drivers working in pairs: one pair sets the polarity of one branch of the garland, and the polarity determines the color of the glow. Obviously, to work with this, we need pulse-width modulation, and quite specific - asymmetric, that is, during each period each of the polarities can turn on for different times (the simplest case - giving only one polarity, we get only one color of the garland glow) .

    This scheme makes it difficult to use the ready-made PWM module in the forehead (in OpenWRT there is one) - it produces a symmetrical signal, that is, the maximum that can be achieved with it is to adjust the brightness of the entire garland in all colors at the same time, without the ability to control colors independently. Therefore, PWM must be done by yourself.

    PWM is, at a basic level, just a quick jerking of the processor legs, in the simplest case, in this cycle:

    while (true)
    {
      if (value > 0)
        gpioSet(GPIO_NUMBER, 1);
      for (int i=1; i<=100; i++)
        if (value == i)
          gpioSet(GPIO_NUMBER, 0);
    }
    


    The meaning, I think, is obvious: with the value "value" we set the desired duty cycle, expressed in our case in the brightness of the LEDs of the garland (we do not touch the polarity reversal and color management yet).

    The standard content of the gpioSet (int gpio, int level) function in Linux is pulling the GPIO legs through the standard sysfs interface: to set the output N to one, write the unit to the pseudo-file / sys / class / gpio / gpioN / value. Before that, write the number N in / sys / class / gpio / export so that gpioN becomes available to the user, as well as the word “out” in / sys / class / gpioN / direction, to set the mode of this GPIO to “output”.

    There are thousands of descriptions of this procedure, for example, one of them .

    void gpioSet(int gpio, int value)
    {
      sprintf(buf, "/sys/class/gpio/gpio%d/value", gpio);
      fd = open(buf, O_WRONLY);
      sprintf(buf, "%d", value);
      write(fd, buf, 1); 
    }
    


    The method is simple, universal (it is difficult to find a language from which it will not be possible to write to a file), but it is very slow . If we use it to control our garland, then in the end it will take so much time to pull four legs (two pairs of half-bridges) that the PWM frequency on the Black Swift with its 400-MHz processor will be about 100 Hz, and at low brightness the LEDs will be clearly and it’s unpleasant to flicker - there will generally begin to skip measures.

    Fortunately, there is a better way. Fortunately, it’s not that difficult — it’s simpler. Straightforward.

    void gpioSet(int gpio, int value)
    {
      if (value == 0)
        *(GPIO_ADDR + 4) = (1 << gpio);
      else
        *(GPIO_ADDR + 3) = (1 << gpio);
    }
    


    The trick is that the GPIO in the processor is controlled through registers - all operations with them are done through them. And the registers are mapped to regular memory. And ordinary memory is available to us directly through the usual device / dev / mem. Accordingly, all we need to do is map the desired small piece / dev / mem into memory (memory into memory ... oh well) using mmap, and then pull the necessary bits in this piece.

    if ((m_mfd = open("/dev/mem", O_RDWR) ) < 0)
    	return -1;
    m_base_addr = (unsigned long*)mmap(NULL, GPIO_BLOCK, PROT_READ|PROT_WRITE, MAP_SHARED, m_mfd, GPIO_ADDR);
    close(m_mfd);
    


    The mapping of registers to memory is described in the datasheet on the processor; in the case of AR9331, addresses start with GPIO_ADDR = 0x18040000 ( page 65 ). We are also interested in the addresses at +3 and +4 to the base one - writing a unit in a bit corresponding to the GPIO number sets GPIO to 1 at the first address and resets to 0 at the second (if anything, there is also a register at +2, which can reset and install GPIO - you need to write either 0 or 1 to the desired bit). The GPIO direction is set in bits at the base address - 1 for output, 0 for input.

    Nota bene: some GPIOs are multifunctional, this is set in a separate register - and until, for example, you turn off UART on GPIO 9, it will not work like regular input / output. In addition, GPIOs 13 through 17 cannot be used as inputs.

    Speed? Twitching by four GPIOs in double asymmetric PWM - approximately 4.5 kHz. Against about 100 Hz when working with sysfs, I recall.

    PWM period adjustment with nanosleep



    Obviously, we don’t need such a speed of daisy-chain control - everything above 100 Hz is perfect for us, especially if it doesn’t “lose” clocks at low brightness (and it won’t work directly with GPIO). It is necessary to introduce a delay. Standardly short delays are entered using the nanosleep (& tw, NULL) function:

    struct timespec tw;
    tw.tv_sec = 0; // секунды
    tw.tv_nsec = 10000; // наносекунды
    while (true)
    {
      if (value > 0)
        gpioSet(1);
      for (int i=0; i<100; ++i)
      {
        if (value == i)
          gpioSet(0);
        nanosleep(&tw, NULL);
      }
    }
    


    Theoretically, here we should get a delay of 10 μs for each PWM clock cycle, a total of 100 clock cycles - a total of 1 ms, or a PWM frequency of 1 kHz (not taking into account the overhead of jerking the foot). Compile, run ... and get about 140-150 Hz.

    The problem is that the minimum regularly maintained nanosleep period in OpenWRT and on such a processor is about 60 μs. That is, even if you pass tw.tv_nsec = 0 to the function, it will still slow down the thread by 60 μs.

    Fortunately, there is a primitive, not very accurate, but working way to deal with this: calling nanosleep (NULL, NULL) takes about 3 μs.

    void nsleep(unsigned long nsecs)
    {
    	if(nsecs == 0)
    	{
    		nanosleep(NULL,NULL);
    		return;
    	}
    	struct timespec ts;
    	ts.tv_sec=nsecs / 1000000000L;
    	ts.tv_nsec=nsecs % 1000000000L;
    	nanosleep(&ts,NULL);
    }
    const int nsleep0_factor=3000;
    const int nsleep1_factor=70;
    void _usleep(unsigned long usecs)
    {
    	if (usecs == 0)
    		return;
    	unsigned long value = (usecs*1000) / nsleep0_factor;
    	if (value > nsleep1_factor)
    	{
    		nsleep((value-nsleep1_factor) * nsleep0_factor);
    		value = value % nsleep1_factor;
    	}
    	for (unsigned long i=0; i < value; ++i)
    		nsleep(0);
    }
    


    As a result, calling _usleep less than 70 microseconds, we will not call nanosleep normally, but simply scroll many, many times nanosleep (NULL, NULL), each call of which will take 3 microseconds. It’s rough, but we don’t need it more precisely for our purposes (if you need high-quality PWM, you need to do it all the same hardware or software on a system where you guarantee real-time mode - for example, hook up a regular ATTiny to Black Swift via UART).

    Well, in general, the basic bricks are ready - we can make a completely stable software PWM with a frequency of a hundred or two hertz.

    You can also recall that we control two pairs of half-bridges with this PWM, but this is already commonplace:

    for (int i = 0; i < 100; i++)
    {   
    	for (int j = 0; j < 2; j++)
    	{
    		if (i == floor((float)gpioValuePos/brightness))
    		{
    			gpioSet(gpio[2*j], GPIO_OFF);
    			gpioSet(gpio[2*j+1], GPIO_OFF);
    		}
    		if (i == (100 - floor(((float)gpioValueNeg/brightness))))
    		{
    			gpioSet(gpio[2*j], GPIO_OFF);
    			gpioSet(gpio[2*j+1], GPIO_ON);
    		}
    	}
    	_usleep(PWM_PERIOD);
    }
    


    Where gpioValuePos and gpioValueNeg are the values ​​of two polarities (with the condition that their sum should not exceed 100, of course), brightness - this is what we laid down in advance for the ability to adjust the brightness of the entire garland immediately. Installation on two GPIOs of the same level is equivalent to turning off the garland.

    Bells and whistles



    What else do we need from the application that controls the garland?

    Firstly , the brightness should somehow change in time, and it is better that there are several programs. You can make your own scripting language, I did it easier - I inserted a three-dimensional array into the code, inside which 6 programs are stored, in each 4 stages, in each two pairs of numbers - the initial brightness value of one polarity and the speed of its change. Yes, both branches of a garland work synchronously for me.

    int prg[PROGRAMS][4][4] = {
    	{ {0, 1, 0, 0}, {99, -1, 0, 0}, {0, 0, 0, 1}, {0, 0, 99, -1} },
    	{ {0, 1, 0, 0}, {0, 0, 0, 1}, {0, 1, 0, 0}, {0, 0, 0, 1} },
    	{ {99, -1, 0, 1}, {0, 1, 99, -1}, {99, -1, 0, 1}, {0, 1, 99, -1} },
    	{ {99, 0, 0, 0}, {99, 0, 0, 0}, {99, 0, 0, 0}, {99, 0, 0, 0} },
    	{ {0, 0, 99, 0}, {0, 0, 99, 0}, {0, 0, 99, 0}, {0, 0, 99, 0} },
    	{ {49, 0, 50, 0}, {49, 0, 50, 0}, {49, 0, 50, 0}, {49, 0, 50, 0} }
    };
    


    That is, for example, the first program - the brightness of the "+" channel gradually increases, then it gradually decreases, then the brightness of the "-" channel also increases and decreases. Simply put, one color fades in and goes out, then another one fades in and out just as smoothly. And in the latter, all the LEDs of all colors are constantly lit.

    Secondly , since we have a whole Black Swift here, let's do Wi-Fi control? And then the smartphone application, in the end, what kind of geek are you, even if you even have a Christmas tree garland without your IP address? In general, you need to make an interface to the regular web.

    Technically, the easiest way is to make control through UNIX-socket a pseudo-file, commands to which you can push at least from PHP, at least from the command line (with the socat utility).

    mode_t mask = umask(S_IXUSR | S_IXGRP | S_IXOTH);
    int s, s2, len;
    unsigned int t;
    struct sockaddr_un local, remote;
    if ((s = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
    {
        printf("Socket error\n");
        return -1;
    }
    int flags = fcntl(s, F_GETFL, 0);
    fcntl(s, F_SETFL, flags | O_NONBLOCK); 
    local.sun_family = AF_UNIX;
    strcpy(local.sun_path, "/tmp/treelights.sock");
    unlink(local.sun_path);
    len = strlen(local.sun_path) + sizeof(local.sun_family);
    if (bind(s, (struct sockaddr *)&local, len) == -1)
    {
        printf("Socket bind failed\n");
        return -1;
    }
    if (listen(s, 5) == -1)
    {
        printf("Socket listen failed\n");
        return -1;
    }
    umask(mask);
    


    Everything here is practically template, I’ll only explain that operations with umask at the beginning and end are needed on systems where the web server and our application work under different users. In OpenWRT, by default this is not so, everything is there under root. The non-blocking operation of the socket is also fundamental - otherwise the first call to it will cause everyone to stand up and wait until something falls into the socket.

    Inside our PWM, we insert the processing code for the one falling into the socket:

    s2 = accept(s, (struct sockaddr *)&remote, &t);	
    if (s2 > 0)
    {
        int i = recv(s2, sockinput, 25, 0);
        sockinput[i] = 0; // null-terminated string
        close(s2);
        cmd[0] = strtok(sockinput, "- \n");
        cmd[1] = strtok(NULL, "- \n");
        if (strcmp (cmd[0], "brightness") == 0) // set brightness
        {
            brightness = atoi(cmd[1]); // 1 is maximum, 2 is half-brightness, etc.
        }
    }
    


    The idea, I think, is clear. We throw in the “brightness 2” socket - we get the halving of the brightness of the garland. Processing of any other commands is added in the same way.

    What else to add? For the convenience of manual launch during debugging - let the program correctly respond to Ctrl-C and other requests to move:

    do_exit = 0;
    static struct sigaction act; 
    sigemptyset (&act.sa_mask);
    act.sa_flags = 0;
    act.sa_handler = SIG_IGN;
    sigaction (SIGHUP, &act, NULL);
    act.sa_handler = cleanup;
    sigaction (SIGINT, &act, 0);
    act.sa_handler =  cleanup;
    sigaction (SIGTERM, &act, 0);
    act.sa_handler =  cleanup;
    sigaction (SIGKILL, &act, 0);
    while (!do_exit)
    {
    // тут по-прежнему вся наша ШИМ
    }
    


    And add the function:

    void cleanup(int sig)
    {
        do_exit = 1;
    }
    


    Now a signal arriving from the OS with a request to interrupt will switch the do_exit variable to 1 - and the PWM cycle will end (do_exit, of course, it would be nice to declare it as a global variable).

    In general, that’s all. We have ready-made bricks for building an asymmetric PWM with brightness control, several work programs and web-based control.

    The result lies here: https://github.com/olegart/treelights , including the Makefile for OpenWRT and for the project itself (this is no different from regular Makefiles for building software under any Linux). Video at the request of our readers will be in the third part (although what you, garlands have not seen, or what?).

    Of course, all of the above applies to any OpenWRT router and nanocomputer, with one caveat: for Black Swift, we build a core with a timer at 1000 Hz versus the usual 100 Hz and with preemptive multitasking enabled. Without it - on standard cores - PWM can be slower or less stable under any load on the system.

    However, about the assembly of their firmware for OpenWRT - another time.

    Also popular now: