• Calling the Control Loop

    Luke Thompson04/18/2021 at 15:45 0 comments

    [I wrote this project log almost a year ago and forgot to publish it. I realized that trying to do position control on a brushed dc motor was not going to work the way I would have liked, so I stopped pursuing that approach. I may come back to the problem again in the future, this time with stepper motors.]




    To control the motors, we need a control loop to run at some fixed frequency that checks the current position/velocity and updates the pwm duty cycle based on what the desired position/velocity is. Ideally this loop would have accurate timing and run at higher frequencies.

    Because of these requirements, we can't just place the control function in the Arduino loop, since we don't know the frequency it will run at, and since other functions might take a variable amount of time, there will be a large amount of jitter.

    The second option we unfortunately have to eliminate is writing a freeRTOS task and using the 'vTaskDelayUntil' to set our timing. While this seems promising at first, this function can only delay in integer number of freeRTOS 'ticks' and will only start at the next tick. In the Arduino build of the esp-idf, this is set to one millisecond, which isn't too bad, but limits the possible frequencies to (1000 Hz, 500 Hz, 333 Hz, 250 Hz, etc.) and will have a jitter potentially bigger than one millisecond.

    If we have already eliminated these 2 options, what alternatives are there? There are 3 different approaches that had potential: use a interrupt service routine (ISR), use the esp-idf high resolution timer, or use a semaphore to signal a task from an ISR.

    ISR

    Using an ISR sounds like the perfect solution in theory, and thus was the approach I spent the most time pursuing. The esp32 General Purpose Timer allows us to easily setup an interrupt at a fixed, configurable rate. We then can write a controller in that interrupt that will perform the calculations and update the duty cycle as needed. It is at this point that we run into the 2 (maybe 3) issues that makes this approach hard to implement. The floating point unit (FPU) is disabled in interrupts by default, and only  functions in RAM can safely be called from the interrupt.

    Getting the FPU enabled wasn't too difficult thanks to this thread on the esp forum. (If we were build the esp-idf from source, we could enable this with this configuration option.)

    uint32_t cp0_regs[18];    // FPU enable code from: esp32.com/viewtopic.php?t=1292#p5936
    void IRAM_ATTR controlLoop() {
        // // Save and enable the FPU if needed
        // get FPU state
        uint32_t cp_state = xthal_get_cpenable();
    
        if(cp_state) {
            // Save FPU registers
            xthal_save_cp0(cp0_regs);
        } else {
            // enable FPU
            xthal_set_cpenable(1);
        }
    
        // DO WORK HERE
    
        if(cp_state) {
            // Restore FPU registers
            xthal_restore_cp0(cp0_regs);
        } else {
            // turn it back off
            xthal_set_cpenable(0);
        }
    }
    

    The other issue is a bit more insidious. If the cpu is in the process of reading from the flash (such as when the web server serves a webpage from SPIFFS) when the ISR is triggered, then the ISR is unable to read anything from memory, including functions. If this situation happens, the esp32 core panics with a message such as: "Guru Meditation Error: Core 1 panic'ed (Cache disabled but cached memory region accessed"

    The simplest way to address this is to add the "IRAM_ATTR"  attribute to any functions called by the ISR. This works ok if the only functions that we are calling are ones we wrote (though we do give up some RAM in the process), but breaks down if we need to call library functions such as the 'pcnt_get_counter_value' and 'mcpwm_set_duty'.

    In order to access the hardware peripherals, we need to access them directly. To figure out how to do that, I read through the source for the esp-idf drivers (ex pcnt) and tried to follow function calls until I found where the values were being directly written to (ex pcnt). I was able to figure that out for the pulse counter, but I wasn't able to figure...

    Read more »

  • Websockets Debug

    Luke Thompson04/02/2021 at 05:19 0 comments

    As with most projects, they go through phases of active work, and long periods of hibernation. This one is no different, but progress has been made since it started.

    The initial work was done purely in the esp IDF which has its advantages, but since I would like to make this easy to use and modify for anyone, I decided to switch to the Arduino framework. This also served as an excuses to rewrite a large portion of the code from scratch using what I had learned from the previous attempts. In this project log, I'll focus on the debugging framework that I wrote which uses websockets to display real-time variables and allows variables to be adjusted on the fly.

    I was inspired by the work [CNLohr] did with the esp8266 using websockets and I wanted to create my own version so I could better understand how it works and to implement a few tweaks. I wanted to have a framework which allowed me to easily track values and see how they changed over time. The driving force behind this idea was to make the control loop tuning easier by watching a graph of the response and adjust gains in real time. But having that functionality makes other parts of development much easier as well.

    Overview

    The general principle behind how the system operates is to have an array of pointers to the memory addresses of the variables you want to watch and/or adjust as well as their type (float, int, etc.) and a name of some sort. Then, when a websocket connection is made, that data can be transmitted to the webpage, where a javascript program unpacks the data, displays it in a table, and graphs it. If we stick with 32 bit variable types (floats, int32_t, uint32_t) then all memory reads/writes are atomic and we don't have to worry about potential race conditions.

    Microcontroller Side (C++)

    For now, here is how the C++ side of things works. It is somewhat limited in capability, but is functional enough for now that I can move on to getting other parts of the system working:

    The wsData class consists of 2 main functions. The 'add' functions, and the 'processPacket' which handles calling the other functions ('sendNames', 'sendData', and 'updateVars') as needed. The 'add' function is pretty self explanatory in that it allows you to add a variable to the system. The function takes the address of the variable, and a char array that is used as a title and description of the variable. Since it is a C++ function, we can use overloaded functions to determine the variable type so we don't need to specify directly (unlike the original C/IDF version of this I wrote).

    The add function then populates a struct array with the pointer to the variable, a pointer to the name string, and a type field so that floats and ints can be differentiated.

    When the esp32 receives binary websocket data, it is assumed that it is a command for the wsData class, and the data is passed to the 'processPacket' method. There are 3 types of packets, corresponding to the remaining functions, of the following forms:

    S -- Setup

    The Setup packet is designated by the 'S' opcode in the first byte. It is then followed by a 16 bit number which corresponds to the variables location in the wsData array (and consequently, the order in which it was added). 16 bits is probably a bit excessive, but 255 might be limiting in the future and it only costs an extra byte for 'future-proofing'. As a note, all numbers are stored and transmitted as little-endian since that is what the xtensa cores are and I would rather push complexity into the browser than on the microcontroller.

    Following the number is a single byte which represents the variable type (currently only floats and int32_t are supported). Currently the type is represented by a character, either 'i' or 'f' for integer and float respectively.

    After the variable type, the name is transmitted as a null terminated string. Immediately after the null character follows the 2 bytes for the next variable and the cycle continues until all of...

    Read more »