You have a single-core, single-threaded CPU. This means that at any given time, it's doing exactly one thing. If your application requires it to do several things, then the code has to be designed to switch between all of them. This can be as trivial or as complex as you like.
Normally, the CPU putters around the main loop, doing whatever is in there, and that's all well and good until an interrupt occurs. The interrupt hardware basically forces a function call to the appropriate ISR, even though there's no call instruction for it in the main loop. This unpredictability is where most of the rules come from for writing ISR's.
Whatever time you spend in an ISR is time that the main loop is paused, waiting for the ISR to return. If the main loop is the only one to reset an active watchdog timer (very good practice), then the watchdog will not be reset during that time. If the watchdog times out, it gives you a hard reset. Just like the external reset, but with different flags that you can check on startup. This is probably the "crash" that you heard of.
It's very good practice to use the watchdog, and to only reset it once each trip around the main loop. This forces you to write code that stays responsive. If you need to wait for something, you can set up an event (timer finished, next character received, etc.), and move on. Check periodically for that event or set another interrupt for its completion, and get back to it then. Meanwhile, you continue with whatever else you were doing.
My main structure is typically something like this:
//clear interrupt flags
//global interrupt enable
And my modules are like this:
//hardware and variable init for this module only
//don't use interrupts if polling is good enough
POLLED_INTERRUPT_FLAG = 0;
//non-blocking "ISR" code
void ISR modX_ISR(void)
//okay, this does require an *immediate* response
//spend the absolute minimum time here and get out
The function signatures don't have to be
void, but most of them are. Sometimes I'll have some broad timing in one module that is also used by another, and it's handy to use the return value of one
modX_run() and the arguments of another (or some basic logic) to make that connection. For example:
if (DMX_run()) //includes its own timing, and returns true at the start of each 30Hz interval, otherwise false
I2C_start(); //I2C frames are sync'ed to DMX
I2C_run(); //once started, an I2C frame runs freely until finished
If you study the datasheet, you may also find that the hardware peripherals can be massaged to do what you want without any CPU intervention at all.
Output pulse generation, for example, is a common one. Turn it on, set the peripheral to turn it off some time later, and forget about it. It's typically in the same general area as PWM.