tinygo-org / tinygo

Go compiler for small places. Microcontrollers, WebAssembly (WASM/WASI), and command-line tools. Based on LLVM.
https://tinygo.org
Other
15.48k stars 913 forks source link

panic when Interrupts enabled in RP Pico #2653

Closed bmentink closed 1 year ago

bmentink commented 2 years ago

I am getting: panic: runtime error: nil pointer dereference .. whenever I enable interrupts in my test code. The interrupt handler does not seem to be doing anything .. (do println statements work in the handler?)

Is there any way to find where the Panic is occurring? Is there a good example for using Interrupts on the Pico?

I am essentially doing the following to use interrupts:

//Interrupt pin: comp_A.Configure(machine.PinConfig{Mode: machine.PinInputPullup})

//Set interrupt (presume interrupt is now enabled) comp_A.SetInterrupt(machine.PinRising, handlerA_rising)

// Handler func handlerA_rising(p machine.Pin) { comp_A.SetInterrupt(0, nil)) // Disable interrupt !! not sure if you have to disable interrupt to ensure another one does not happen in this handler ??? ///.... do stuff }

Any idea's

sago35 commented 2 years ago

@bmentink The following example appears to be working correctly.

https://github.com/tinygo-org/tinygo/blob/release/src/examples/pininterrupt/pininterrupt.go

Note that you need to add the configuration for rp2040.

//go:build rp2040
// +build rp2040

package main

import "machine"

const (
    buttonMode      = machine.PinInputPulldown
    buttonPinChange = machine.PinRising
)
bmentink commented 2 years ago

@sago35

Yep, that example works. Guess there is something amiss in code that is called by the interrupt ... without further info from the "panic" I am struggling with how to debug this ...

I have plastered error handling all over the place, but none gets hit ... just the panic ..

Can you answer my other question? I.E Can you have println statements in a interrupt routine?

bmentink commented 2 years ago

With some more investigation, I am getting: machine: no channel available for pin interrupt error..

I have the following code setting up the interrupts .. ( pe() is a print error function wrapping the calls.)

// These functions setup the comparator interrupts
func (d *Device) bemf_B_falling() {
    pe("setupBF", d.comp_B.SetInterrupt(machine.PinFalling, d.handlerB_falling))
}

func (d *Device) bemf_A_rising() {
    pe("setupAR", d.comp_A.SetInterrupt(machine.PinRising, d.handlerA_rising)) // enable this interrupt, disable the rest
}

func (d *Device) bemf_C_falling() {
    pe("setupCF", d.comp_C.SetInterrupt(machine.PinFalling, d.handlerC_falling))
}

func (d *Device) bemf_B_rising() {
    pe("setupBR", d.comp_B.SetInterrupt(machine.PinRising, d.handlerB_rising))
}

func (d *Device) bemf_A_falling() {
    pe("setupAF", d.comp_A.SetInterrupt(machine.PinFalling, d.handlerA_falling))
}

func (d *Device) bemf_C_rising() {
    pe("setupCR", d.comp_C.SetInterrupt(machine.PinRising, d.handlerC_rising))
}

I get these errors, when they are executed: (They are executed in the sequence above)

setupBF machine: no channel available for pin interrupt setupAR machine: no channel available for pin interrupt

How am I filling up channels? As you can see, I am wanting a handler for each of the edges, is this the correct way to do it? These handlers get enabled one at a time, I would have thought that that was possible?

sago35 commented 2 years ago

@bmentink Please write the entire source code so I can look into it some more. Please use Quoting code to do so.

You may also want to use gist.

https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#quoting-code

bmentink commented 2 years ago

@sago35 Sorry, it's 1000+ lines at the moment and commercially sensitive .. What else can I post that will help?

All I can say at this point, is that even just calling one of the above functions that sets' the interrupt results in that error ..

This is the function that calls the above:

// High level BLDC motor commutation function
func (d *Device) bldc_move(en bool) {
    switch bldc_step {
    case 0:
        start_time = time.Now() // Get the current time
        if en == true {
            d.bemf_B_falling()
        }
        d.ch_al()
    case 1:
        comm_time = int64(time.Since(start_time).Microseconds()) // return time between 60deg interrupts, in microseconds.
        if en == true {
            d.bemf_A_rising()
        }
        d.ch_bl()
    case 2:
        if en == true {
            d.bemf_C_falling()
        }
        d.ah_bl()
    case 3:
        if en == true {
            d.bemf_B_rising()
        }
        d.ah_cl()
    case 4:
        if en == true {
            d.bemf_A_falling()
        }
        d.bh_cl()
    case 5:
        if en == true {
            d.bemf_C_rising()
        }
        d.bh_al()
    }
}

It's not even getting as far as the interrupt handler .. the setup is failing.

One of the 6 handler's looks like this:

func (d *Device) handlerA_rising(p machine.Pin) {
    pe("handlerAR", d.comp_A.SetInterrupt(0, nil)) // Disable interrupt
    d.motorStep(d.comp_A)
}
bmentink commented 2 years ago

I think the problem is I can't seem to assign two handlers, one for each edge. On the same pin I.E

func (d *Device) bemf_A_rising() {
    pe("setupAR", d.comp_A.SetInterrupt(machine.PinRising, d.handlerA_rising)) // enable this interrupt, disable the rest
}

func (d *Device) bemf_A_falling() {
    pe("setupAF", d.comp_A.SetInterrupt(machine.PinFalling, d.handlerA_falling))
}

Because they refer to the same pin ... I am having issues.

I could have an interrupt handler that responds to both edges, But then how do I tell which edge it is? ... I need to have both edge interrupts active ... at the same time... because I don't know which comes first ..

Seems to be some functionality missing here for this user case ..

EDIT:

I also cannot do this to setup all interrupt handlers at once, it crashes the compiler ...

        pe("setupBF", d.comp_B.SetInterrupt(machine.PinFalling, d.handlerB_falling))
    pe("setupAR", d.comp_A.SetInterrupt(machine.PinRising, d.handlerA_rising)) 
    pe("setupCF", d.comp_C.SetInterrupt(machine.PinFalling, d.handlerC_falling))
    pe("setupBR", d.comp_B.SetInterrupt(machine.PinRising, d.handlerB_rising))
    pe("setupAF", d.comp_A.SetInterrupt(machine.PinFalling, d.handlerA_falling))
    pe("setupCR", d.comp_C.SetInterrupt(machine.PinRising, d.handlerC_rising))
sago35 commented 2 years ago

The following requests have been made, but I don't think anyone has worked on them yet.

2452

sago35 commented 2 years ago

It would be easier to analyze if you could make a small reproduction of the code instead of a part of the code.

bmentink commented 2 years ago

Hmm, I will try, but very difficult as it is quit complex ... and also, there is a lot of real-time stuff going on, so hard to debug .. especially as the only options at this point is printf's ..

Meanwhile I have reduced my interrupt handlers to 3 that react to both edges, However it still crashes the compiler when I try to enable them all at once I.E

        pe("setupAR", d.comp_A.SetInterrupt(machine.PinRising|machine.PinFalling, d.handlerA)) 
    pe("setupBR", d.comp_B.SetInterrupt(machine.PinRising|machine.PinFalling, d.handlerB))
    pe("setupCR", d.comp_C.SetInterrupt(machine.PinRising|machine.PinFalling, d.handlerC)) 
bmentink commented 2 years ago

@sago35 I have found the issue. You can't seem to have a time.Sleep() call in an interrupt function, it causes the panic.

So my attempt at correcting this is to have the interrupt signal a Goroutine by doing the following:

// These are the interrupt handlers
func (d *Device) handlerA(pin machine.Pin) {
    debounce(d.comp_A)
    trigA <- 0
}

then the Goroutine:

func (d *Device) doAPhase() {
    for { // Forever
        select { // Wait for one of the event below
        case <-trigA:
            //led.Set(true)
            for sector := 0; sector < 6; sector++ {
                for i := 0; i < 32; i++ {
                    d.setDutyA(uint32(duty[index[sector][0]][i]) >> 1) // These three phase duty needs to be loaded into the correct duty register
                    time.Sleep(time.Microsecond * 2)
                }
            }
            led.Set(true)
        case <-quitA:
            return
        }
    }
}

The above time.Sleep() causes a panic and the LED is not lit ... if I comment it out the LED is lit and no panic.

So why can't I have sleeps in a Goroutine either ?? (The channels (trigA and quitA) are declared globally ... )

aykevl commented 2 years ago

I have found the issue. You can't seem to have a time.Sleep() call in an interrupt function, it causes the panic.

Correct. There is no way we could support this in a safe way. There are multiple reasons this is difficult:

Instead of doing a debounce by sleeping, have you tried doing it via time.Now? That function should still work in interrupts.

So my attempt at correcting this is to have the interrupt signal a Goroutine by doing the following:

// These are the interrupt handlers
func (d *Device) handlerA(pin machine.Pin) {
  debounce(d.comp_A)
  trigA <- 0
}

This probably works... most of the time. It will still panic if trigA doesn't have space to receive the data. For safety, it may be better to do this using a non-blocking select:

select {
case trigA <- 0:
    // send was successful
default:
    // send failed: channel is full
    // how this should be handled depends on the application (ignore, panic, reboot, whatever)
}

Do you have a debugger? I can very much recommend a debugger to debug these issues. For example, you could set a breakpoint on runtime._panic and if it is hit, do a backtrace to see where in the code the panic is triggered.

bmentink commented 2 years ago

Is there a HOWTO somewhere on adding a debugger to VSCode for TinyGo on Linux? I used to use OpenOCD for GDB, but I am guessing there has to be some other tool?

aykevl commented 2 years ago

There is this PR that I still need to take a look at: https://github.com/tinygo-org/vscode-tinygo/pull/4

What I normally use is tinygo gdb (which you use just like tinygo flash but it'll drop you into a GDB shell). It may need a little bit of configuration depending on your board but many boards with on-board debugger are already supported. This may be helpful: https://tinygo.org/docs/guides/debugging/

bmentink commented 2 years ago

Thanks, It will be great to see it integrated into VSC ..

bmentink commented 2 years ago

By the way. I have my application mostly working now.

I have actually got it going by using a custom ticker(since time.Tick() is not supported), my ticker sends to a channel continuously at a rate determined by the receiver function. The overhead on this uP is a round-trip of 2uS. Means I can run motors over 15,000 RPM if I want.

aykevl commented 2 years ago

I have actually got it going by using a custom ticker(since machine.Tick() is not supported), my ticker sends to a channel continuously at a rate determined by the receiver function. The overhead on this uP is a round-trip of 2uS. Means I can run motors over 15,000 RPM if I want.

Note that if you do this outside interrupts, there is a risk that something else (an interrupt, the GC, or another goroutine) does something in that time and increases the duration. If you want reliable execution, you need to control the motors entirely from within interrupts. I don't know the hardware, so I can't tell whether it's critical to always be on time. Just noting it in case :)

Great to hear that TinyGo works for you!

bmentink commented 2 years ago

@aykevl Thanks. Yep, aware of the variations, but in this application, seems to work well. I can't use Interrupts by themselves to drive the motor, as TinyGo does not allow me to setup both edges at once, So what I have done is run a free running timer as mentioned, and am syncing that periodically with the positive edge of the commutation signal only. As a side effect it allows open-loop startup to happen automatically ..

EDIT: By the way, Is there any way to control the GC, As a test,I did run runtime.GC() evey 500ms, and it did glitch the motor. Any way to disable, or not use the heap ... etc Also, since my regular tick method is based on time.Sleep(), and that is interrupt driven, I presume, how does the GC not affect external interrupts, but effects timer based ones?

aykevl commented 2 years ago

You may be interested in this page: https://tinygo.org/docs/concepts/compiler-internals/heap-allocation/

You can disable the GC entirely using -gc=leaking. It will continue to allocate and never de-allocate. This is fine for applications that never allocate memory after initialization, but in that case you would also never see the GC run because the GC only runs when the heap is full.

Also, since my regular tick method is based on time.Sleep(), and that is interrupt driven, I presume, how does the GC not affect external interrupts, but effects timer based ones?

Interrupts run at a higher priority. They interrupt whatever the chip is doing (including possibly a GC run, which is why it's not allowed to allocate memory inside an interrupt). In contrast, time.Sleep() is deeply integrated with the scheduler. Once all goroutines are sleeping, the scheduler will set up a timer and go to sleep. Once enough time has passed, the timer awakes the chip again and the scheduler continues running goroutines. So while the timer is interrupt based, the scheduler still runs at the base (lowest) priority.

bmentink commented 2 years ago

@aykevl Many thanks for the explanation. I tried the -print-allocs=. flag, but see very little heap allocations in my code. There is a heap allocation when main() calls my driver's main function, but there is no allocations actually in the driver. So the GC runs I am seeing (every 1..2 minutes) must be TinyOS core related .. I will have a play with the -gc=leaking flag ..

EDIT: Well that flag stopped it glitching .... for awhile ... then I got an out-of-memory error panic ..

I am at a bit off a loss as to how to proceed. If I make everything external interrupt driven, as you suggest, then since I can't make any heap allocations in the interrupt routine, I am very limited to what I can achieve ..

I am used to doing all my real-time programming using purely static memory allocations, so this is a bit of a challenge to me .. The question is: Can TinyGo be used for real-time applications such as mine ?

EDIT2: I removed some fmt.Printf() statements from main() (this occur after I started my BLDC motor driver with a goroutine), and I don't get the glitches anymore. This is with my code compiled without -gc=leaking flag ....

In essence I was doing:

       go bldc.MotorStart()      

    for { // forever loop
                comm := bldc.GetRPM()
        rpm := (1.0 / (float32(comm) * 1e-6)) / 6.0 * 60.0
        fmt.Printf("Comm=%d RPM=%.2f\n\r", comm, rpm/POLES)
        time.Sleep(time.Millisecond * 500)
    }
bmentink commented 2 years ago

Is there a way to get an interrupt on PWM timer rollover? That would help me achieve what I want ..

deadprogram commented 1 year ago

Closing since this appears to be resolved. Please reopen if needed. Thanks!