If you can use an operation system, it is easier. You may assign a task that scans the buttons. UI work on other task. And listens incoming messages (such as using mailbox) also updates the screen. Wait's are not done by loops, but really handled by OS.
But if this is beyond your aim, the responsiveness may be based on those:
1) UART communication is controlled by interrupt. If you are passing ASCII code, like terminal communication, you can buffer incoming and outgoing to free the main control loop waiting communication to complete.
2) Maim loop consists of state machines. In each state you do one thing. You do not introduce any wait loop. Delaying is made by a timer that ticks in periodic fashion, and a counter that tracks how long in that state the machine should stay..
for instance, the main loop shall like this:
while(1)
{
processTimer();
processUI();
processButtons();
}
processTimer checks the timeout of the timer, and sets timerTick value to 1 if timeout happens, for just 1 loop.. that way you may increment each state machine's own timer..
suppose you are blinking a text, and processUI module is something like this:
void handleMainScreen()
{
if (timerTick)
{
if (++flashCounter == 500)
{
flashState = !flashState;
if (flashState)
paintInverted();
else
paintNormal();
}
}
}
void processUI()
{
switch (uiState)
{
case US_MAINSCREEN: handleMainScreen(); break;
case US_SOMEVALUE: handleSomeValue(); break;
...
}
}
As you see, there is no wait loop that prevents other parts of the program to hang.. Timing is done by "timerTick" variable, that is set for just one loop when periodic timeout (for instance in each 1ms) happens.. You may use same timerTick to check the buttons in processButtons function. Use simple filtering to prevent glitches etc..
It is a lot easier if you use OS though.. But it also has some learning curve and daemons in it aswell
..