Introduction to Microcontrollers - More Timers and Displays

Mike SilvaOctober 15, 20131 comment

Quick Links

Building Your World Around Timers

By now you have seen four different ways to use timers in your programs.  Next we will look at some ways to produce the effect of multiple parallel streams of work in your program with the help of timers.  This effect is only an appearance, not a reality, since a single microcontroller (one core) can only run a single thread of code.  However, since microcontrollers are so fast in relation to a great many of the tasks to which they are applied, we can use that speed to achieve the effect of parallel threads or tasks.  A few common parallel tasks a typical embedded system might require are these:

  • Check for user input
  • Update a display
  • Execute one or more state machines
  • Read new ADC values
  • Generate new PWM outputs
  • Send and receive messages

What needs to be true for all such tasks is that on average the time to complete one iteration of the task is less than the period at which the task must run.  Sometimes this is not an issue - we might check user input once every 10ms and our user input code might only take 20us to run, or 1/500 of our user input task period.  That leaves plenty of time for other code to run.  If, on the other hand, a task takes on average longer to run than the period at which it must run, then you either need to shorten the task (better algorithm, faster hardware) or increase the period (e.g. only calculate your control loop every 20ms instead of every 10ms).  The same applies, of course, to the sum total time of all tasks - if that sum total prevents any task from running at its assigned rate, then the system must be sped up or otherwise redesigned.  

One Loop To Rule Them All

A simple but often effective structure for an embedded solution is the loop+interrupts structure, sometimes called the "cyclic executive" since it cycles around a loop executing various tasks as they are needed.  We have already seen this structure in some of our interrupt-driven programs.  A main loop executes continuously, and each time through it tests for various events to determine what to do next.  These events will typically be the expiration of time deadlines or the setting of flags as a result of interrupts or other code execution.  As long as the loop can repeat often enough so that every task gets enough timely attention, this will be a suitable solution.

The loop chooses what to do next based on whatever flags or data it examines each pass.  In fact, we can even impose a sort of priority on our tasks by putting the tests and code for higher priority tasks at the top of the loop.  If we write the loop (or even just a section of the loop) so as to only do at most one task action per loop, the tasks at the top of the loop (or section) always get first shot at running if their run condition is true.  We will show an example of this later.

Time For You, Not For Me

There are a few ways that different tasks can run at different intervals all based on the operation of a single timer.  Here is one method that is quite flexible.  A timer generates an ongoing tick and the timer tick ISR continuously updates a time value in memory.  Each task is assigned a deadline after which it must run, and for each pass through the loop each task compares the current time value (the value which the timer tick ISR keeps updating) with its deadline time.  If the current time value is after the deadline time, the deadline has passed and the task code is executed.  As part of the running of the task, it typically assigns itself a new deadline for the next time it should run.

For our example we will keep our running time in milliseconds, with our timer ticking (and updating a 16-bit millisecond value) every 2ms (nothing magic about this number, it's just a good example figure).  This will let us time intervals out to 32+ seconds (why not 65+? - you'll see in a bit).  Being able to time things out to 32+ seconds, with a precision of 2ms, will be useful for a great number of typical embedded tasks, but these numbers are not set in stone and you can certainly come up with your own.  For longer intervals you can always keep a running seconds counter that is updated every 1000ms, or you can use a 32-bit running time variable, which makes the deadline checking a little more computationally expensive on an 8-bit device, but lets you time out to a few million seconds.  Of course on a 32-bit device none of this is even an issue - 32-bit architectures make an awful lot of things not even issues, which is one of the big factors in their favor.

First note how simple our 2ms timer tick ISR is:

ISR(TIMER1_COMPA_vect)
{
  MS_value += 2;
}

Since our timer tick happens every 2ms, we simply update our running time value (MS_value) by adding 2.  Having our timer value represent milliseconds rather than just some arbitrary tick time is a concession to readability and programability, but one could just as easily increment the time value and call it a tick count, regardless of the timer tick interval.  Personally I much prefer to think in terms of microseconds and milliseconds and seconds rather than ticks.  Note that our running time value is allowed to roll over exactly as we allow a leapfrog timer to roll over, and for the same reasons.

What we need now is a deadling-comparing function.  Given a deadline, we want to know whether the running time has reached or gone past that deadline.  We can't just check to see if the running time is equal to the deadline because we may not get around to checking for the deadline soon enough, because other task code has been running.  That is, if e.g. we're checking for a deadline of 2000ms, we may not get around to checking that deadline until MS_value has advanced to 2002 or 2004.  What we need is to be able to know if the running time is equal to or after the deadline (not hard), and we need that calculation to work even if the running time has rolled over (harder).  A simple example shows the problem.  Suppose the current running time is at 65,000ms, and a deadline has been set to 1536ms after that, or (after current time rollover), 1000ms.  What is needed is a calculation that recognizes that 1000 is "greater," that is, later in time, than 65,000, and all the timer values between 65,000 and (1000-1).  The magic of 2s complement math provides us a simple solution.  Here is the function:

u8 deadline_reached(u16 dl)
{
  return ((int16_t)(MS_value - dl) > 0) ? 1 : 0;
}

By turning the result of MS_value - dl (current time - deadline time) into a signed value, we can check if the current time is before or after the deadline time simply by looking at the sign of the result.  This works regardless of the unsigned magnitudes of either MS_value or dl, with the one restriction that no deadline time can be set more than 2^15-1, or 32767, ticks away from the current time (for 16-bit time values).  This is why we can only have delays out to 32+ seconds with 16-bit time values, rather than out to 65+ seconds, but it's a very small price to pay for the utility achieved.  Of course the function could be inlined or turned into a macro if you like, to increase the speed of deadline checking.

There's only one problem with our deadline_reached() function - on an 8-bit device, it's broken.  It's that old interrupt data-corruption hobgoblin again.  What if the timer tick ISR changes MS_value while the deadline_reached() function is accessing it to perform the subtraction?  Mayhem!  So for an 8-bit device, our function needs to protect against this possibility, such as:

u8 deadline_reached(u16 dl)
{
  cli();
  u16 temp_ms_value = MS_value;
  sei();
  return ((int16_t)(temp_ms_value - dl) > 0) ? 1 : 0;
}

Now MS_value cannot change while we are accessing it, which is what is required to avoid data corruption.

To generate a new deadline we simply add the desired time interval to the current time, like this:

u16 make_deadline(u16 t)
{
  return (MS_value + t);
}

To generate an immediate running of some task code, we can just set its deadline to the current time using make_deadline(0) - the next time through the loop, that deadline will be recognized as reached, and the task will execute.  To create periodic deadlines we just add the time interval to the current deadline to get the next deadline - again very similar to leapfrogging with hardware timers.  This similarity should not be surprising since all we are doing here is extending the way hardware timers work into a hybrid hardware-software timer.

Note that the deadline returned by make_deadline() is biased to the short side.  This is because MS_value may advance at any time after creating the deadline.  In particular, make_deadline(1) may return a deadline that immediately is reached because MS_value has just advanced after the addition is performed.  In general, the real deadline returned by make_deadline(N) will be between N-1 and N ms in the future.  This is not due to any error, but is simply a result of unavoidable uncertainty at the level of one timer count (one MS_value count in this case).  Any asynchronous access of a timer (an access not synchronized with the timer operation) will run into the same issue.  If the deadline short-side bias is a problem, just call make_deadline with t+1 instead of t.

Using A Timer For Delays

If our current time is scaled to milliseconds, here is another way to create a ms_delay() function, and one that doesn't depend on magic numbers or uC clock rates.  Well, there is dependence on the clock rate, but that will be taken care of by the calculations for the timer so as to generate a tick exactly every N milliseconds.  Anyway, here is the simple timer-based ms_delay():

void ms_delay(u16 d)
{
  u16 dl = make_deadline(d+1);
  while (!deadline_reached(dl))
    ;
}

We are simply setting a deadline based on the delay time, and then waiting for that deadline to be reached.  The reason we use d+1 for the deadline calculation is to avoid the short-side bias discussed above.  That is, by using make_deadline(d+1), the delay will be d <= delay <= d+1.  Without using d+1 the delay will be d-1 <= delay <= d.

32 Bit Timers - A Whole Different World

None of the parts we're working with now have 32-bit timers, but many other families do, such as the NXP family.  32-bit timers let you e.g. measure microseconds out to 2000 seconds (over 30 minutes), assuming you can choose an appropriate prescale value for your incoming prescale clock.  One way to look at this is that instead of 16 bits of hardware timer followed by 16 bits of software millisecond counter, everything can now be done in 32 bits of hardware timer.  With a microsecond-resolution clock you can also use the wait-for-deadline method to measure smaller time intervals, such as the 50us delay needed for an LCD display if not using the busy flag.

Revisiting An LCD Display

At this point we'll finally take a look at the STM32 version of the LCD code, with some changes from the AVR version.

//
// lcd.c
//

#include <stm32f10x.h>
#include "lcd.h"

extern void set_multiple_GPIO(GPIO_TypeDef * p_port, int first_bit, u32 val, int num_bits);
extern void set_GPIO(GPIO_TypeDef * p_port, int bit, u32 val);

#define LCD_USE_BF

void lcd_strobe(void)
{
  LCD_PORT->BSRR = LCD_E;
  nano_delay();       //tiny_delay(LCD_STROBE);
  LCD_PORT->BRR = LCD_E;  // must be >= 230ns
}

void LCD_PORT_data(u8 d)
{
  LCD_PORT->ODR = (LCD_PORT->ODR & ~LCD_DATA) | (d & 0xf0);
}

#ifdef LCD_USE_BF
void lcd_wait(void)
{
  u8 data;

  u32 d = GPIOC->CRL & 0x0000FFFF;          // strip out PC4-PC7
  GPIOC->CRL = d | 0b0100<<16 | 0b0100<<20 | 0b0100<<24 | 0b0100<<28;  // inputs

  LCD_PORT->BSRR = LCD_RW | (LCD_RS<<16);   // set RW, clear RS (read, cmd)
  
  do 
  {
    LCD_PORT->BSRR = LCD_E;                 // 1st nybble, read BF
    data = LCD_PORT->IDR;
    LCD_PORT->BRR = LCD_E;
    nano_delay();       //tiny_delay(LCD_STROBE);
    LCD_PORT->BSRR = LCD_E;                 // 2nd nybble, don't read data
    nano_delay();       //tiny_delay(LCD_STROBE);
    LCD_PORT->BRR = LCD_E;
    nano_delay();       //tiny_delay(LCD_STROBE);
  } while (data & LCD_BF); 

  LCD_PORT->BRR = LCD_RW;                   // set to write
  d = GPIOC->CRL & 0x0000FFFF;              // strip out PC4-PC7
  GPIOC->CRL = d | 0b0010<<16 | 0b0010<<20 | 0b0010<<24 | 0b0010<<28;  // outputs
}
#endif

void lcd_send_cmd(u8 cmd)
{
#ifdef LCD_USE_BF
  lcd_wait();
#endif
  LCD_PORT->BRR = LCD_RS;           // cmd
  LCD_PORT_data(cmd);               // write hi 4 bits
  lcd_strobe();
  LCD_PORT_data(cmd << 4);          // write lo 4 bits
  lcd_strobe();
#ifndef LCD_USE_BF
  tiny_delay(90);
#endif
}

void lcd_putc(u8 c)
{
#ifdef LCD_USE_BF
  lcd_wait();
#endif
  LCD_PORT->BSRR = LCD_RS;          // data
  LCD_PORT_data(c);                 // write hi 4 bits
  lcd_strobe();
  LCD_PORT_data(c << 4);            // write lo 4 bits
  lcd_strobe();
#ifndef LCD_USE_BF
  tiny_delay(90);
#endif
}

void lcd_init(void)
{
  set_multiple_GPIO(GPIOC, 1, 0b0010, 7); // PC1-PC7 set to outputs
  GPIOC->BRR = LCD_RW | LCD_RS | LCD_E; // write, cmd, no E pulse

  set_GPIO(GPIOD, 2, 0b0010);   // backlight to output

  ms_delay(15);                 // delay after powerup
  LCD_PORT_data(DISP_INIT);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(5);
  LCD_PORT_data(DISP_INIT);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(1);
  LCD_PORT_data(DISP_INIT);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(1);

  LCD_PORT_data(DISP_4BITS);
  lcd_strobe();                 // pseudo 8-bit command
  ms_delay(1);

  lcd_send_cmd(DISP_CONFIG);
  lcd_send_cmd(DISP_OFF);
  lcd_send_cmd(DISP_CLR);
  ms_delay(2);  // undocumented delay required for Clear display command
  lcd_send_cmd(DISP_EMS);
  lcd_send_cmd(DISP_ON);
  lcd_backlight(1);
}

void lcd_clear(void)
{
  lcd_send_cmd(DISP_CLR);
  ms_delay(2);  // undocumented delay required for Clear display command
}

void lcd_display(int x, int y, const char *str)
{
  int n = LCD_WIDTH - x;
  u8 addr;

  if ((y < 0) || (y >= LCD_LINES))
    return;

  switch (y)
  {
  default:
  case 0:
    addr = DD_RAM_ADDR;
    break;
  case 1:
    addr = DD_RAM_ADDR2;
    break;
  case 2:
    addr = DD_RAM_ADDR3;
    break;
  case 3:
    addr = DD_RAM_ADDR4;
    break;
  }
  lcd_send_cmd(addr + x + 0x80);
  while (*str && n--)
    lcd_putc(*str++);
}

void lcd_backlight(u8 on)
{
  if (on)
      GPIOD->BSRR = LCD_BL;      // turn on backlite
  else
      GPIOD->BRR = LCD_BL;      // turn off backlite
}

Among the changes for the STM32, one of the big ones is the use of atomic GPIO bit set & reset registers to control the display RS, RW and E lines.  You'll even see in one case the BSRR register used to set RW hi and RS lo in the same instruction - convenient!

Another change is the use of a timer-driven ms_delay() function such as we discussed above.  This is combined with a 1ms timer tick which increments MS_value every millisecond, so now, except for nano_delay(), we have no software delays in our code.  This is a good thing in 99 out of 100 cases.

One more change is the function set_GPIO(), which makes configuring GPIO pins easier.  Also notice that the one place I don't use this function is in lcd_wait(), where all 4 data lines must be changed to inputs and then back to outputs.  Here I just do a single access of the associated CFL register for speed, as opposed to 4 function calls.

A Surprise Gotcha! (Or, Why You Need A Scope #37)

In playing with this code I was testing the effect of different optimization settings on the speed of the code.  Switching from no optimization to O1 optimization made a difference of about 3x speedup - impressive!  Switching from O1 to O2 made an even bigger difference - it broke the code.  Suddenly it seemed as if the millisecond interrupt was running twice as fast!  Confused, I toggled one of the LEDs inside the millisecond interrupt, so every time MS_value was incremented, the LED was toggled.  Then I put the scope on the LED.  Since the interrupt was happening every millisecond, I expected to see a square wave with a period of 2ms, but that's not at all what I saw.  Instead, I saw a very brief pulse happening every millisecond.  This meant that every millisecond the ISR was running twice in quick succesion (and thus MS_value was being incremented twice every millisecond instead of once, thus breaking all the system timing)!  It was as if the interrupt flag that was being cleared in the last line in the ISR was not being cleared fast enough, so the interrupt was being triggered again immediately after the ISR returned.  I moved the line to clear the flag to the beginning of the ISR and sure enough the problem went away.  A little research on the internet shows that this is a known issue, related to the difference in speed between the CPU clock and the peripheral clock that controls the GPIO section timing.  With lower optimization settings, enough additional code was being generated by the compiler so that the flag was set before the ISR returned, but at higher optimization settings this extra code was being stripped out, and the problem surfaced.  You will run into such strange optimization-related glitches now and then, it's just part of the game.  So new rule for the STM32 - always clear the interrupt flag at or near the beginning of the ISR.

And then I went and changed over to the SysTick timer which doesn't require clearing the interrupt flag in the ISR.  Not because it was easier, but to introduce the SysTick timer, which is the specialized timer intended for generating a system tick.

Making Use Of 8-Bit Timers

If you're using an AVR or other uC that has both 8-bit and 16-bit timers, you'll find that you often want to save the more powerful 16-bit timers for doing PWM or input capture or more complex timing tasks.  If possible, then, you'd like to use an 8-bit timer for your system tick, so as to leave the 16-bit timers free for such other tasks.  The problem is, with the AVR's limited choice of prescale values, there is not a large choice of tick rates that are an exact integer multiple of 1Hz.  For example, there's no way to get an exact 10ms tick with either an 8MHz or a 16MHz clock, using an 8-bit timer.  However, we can get shorter ticks of 1 or 2ms, which are ideal for the running time method of scheduling.

Not Every Task Is Time-Driven

We now have an easy technique to execute time-triggered tasks in a simple cyclical executive.  Every time through the loop we check deadlines until we find one that has been reached, and then we execute the code for that deadline and calculate the next deadline.  But not all tasks are time-driven.  Many tasks must execute based upon an external event, and these events are not time-based but come whenever they come.  Examples might be a message arriving on a comms channel, or a control signal from some attached device.

Luckily our cyclical executive can handle such event-driven tasks with no problem.  Typically these events will have a related interrupt, and inside the event ISR an event flag will be set to trigger further processing in the main loop.  In the loop it is just as easy to check for event flags as to check for deadlines.

A Cyclic Executive Framework

Here is one example of a framework for a cyclic executive.  Remember that a timer tick interrupt is running at the same time, continuously updating a current time value against which all deadline values get compared.

while (1)
{
  if (deadline_reached(DL1))
  {
    task1();
    DL1 += SOME_CONSTANT_1;
  }
  if (deadline_reached(DL2))
  {
    task2();
    DL2 += some_calculated_value();
  }
  if (flag3)
  {
    task3();
    flag3 = 0;
  }

  if (flag4)
  {
    task4();
    flag4 = 0;
  }
  else if (deadline_reached(DL5))
  {
    task5();
    DL5 += SOME_CONSTANT_5;
  }
  else if (flag6 || deadline_reached(DL6))
  {
    task6();
    flag6 = 0;
    DL6 += SOME_CONSTANT_6;
  }
}

There are a few things to note here.

  • task1 shows a time-driven task with a fixed period.
  • task2 shows a time-driven task with a variable period.
  • task3 shows a flag-driven (event-driven) task.

Note in this case that each of task1, task2 and task3 is given a chance to run each time through the loop.  This is appropriate for higher-priority tasks.

  • task4 shows another flag-driven task.
  • task5 shows another time-driven task with a fixed period.
  • task6 shows a task that is time-driven as well as flag-driven.

Note in this case that only one of task4, task5 and task6 can run each time through the loop.  This is appropriate for lower-priority tasks, especially tasks that may have a longer running time.  By only allowing one of a set of tasks to run in one pass through the loop, it is assured that the higher priority tasks at the top of the loop will have a chance to run in a more timely fashion.

task6 is an interesting example in that it runs at least as often as its period, but it can also be triggered to run immediately.  One place where you might see this behavior is with a display task.  The task may run every 400-500ms to show updated values (time, temperature, etc), but it can also be triggered to run immediately if the user presses a key, or if some other important event must be displayed to the user without delay.

A Simple Example

Here's a simple example, this time for the STM32, using a cyclic executive to maintain two counters, a 100ms counter and a 1-second counter, an LCD display of both counter values, and two buttons, one to reset each of the two counters.  The tasks will be as follows:

  • 100ms counter - time-driven, 100ms
  • 1 second counter - time-driven, 1000ms
  • LCD display - time-and-event-driven, 400ms
  • Button check - time-driven, 50ms

Just to be different we'll use a 1ms timer tick rather than the 2ms tick discussed above, and we'll use the ARM Cortex SysTick timer for the tick - that's what it's there for, and it's actually easier to use than the other timers.  The 400ms LCD display update period is based on a balance between updating the display often enough to quickly reflect the system state, and not updating so quickly that the changing data becomes a jumble.  Every display situation will be unique and call for specific decisions on the rate of display update.  For example, different update rates could be used for the different display lines, or for different operating modes.  The 50ms button check period is chosen to give quick response to any button push.

//
// STM32_LCD2
// 1ms tick
//

#include <stm32f10x.h>
#include <stdio.h>
#include "defs.h"
#include "lcd.h"

#define F_CPU       8000000UL	// 1MHz
#define PRESCALE    8
#define PERIOD      1	// milliseconds
#define TCLKS       ((F_CPU/PRESCALE*PERIOD)/1000)

void set_GPIO(GPIO_TypeDef * p_port, int bit, u32 val)
{
  if (bit < 8)
  {
    u32 port_data = p_port->CRL & ~(0b1111 << (4*bit));   // clear out config for this bit
    p_port->CRL = port_data | (val << (4*bit));           // add in new config for this bit
  }
  else  // hi bits
  {
    bit -= 8;
    u32 port_data = p_port->CRH & ~(0b1111 << (4*bit));   // clear out config for this bit
    p_port->CRH = port_data | (val << (4*bit));           // add in new config for this bit
  }
}

void set_multiple_GPIO(GPIO_TypeDef * p_port, int first_bit, u32 val, int num_bits)
{
  while (num_bits-- != 0)
  {
    set_GPIO(p_port, first_bit, val);
    first_bit++;
  }
}

int main(void)
{

  RCC->CFGR = 0;                      // HSI, 8 MHz, 

  RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // enable PORTA
  set_GPIO(GPIOA,  8, 0b0010);        // CNF=0, MODE=2 (2MHz output) PA8 LED
  set_GPIO(GPIOA, 11, 0b1000);        // button inputs, pulldowns
  set_GPIO(GPIOA, 12, 0b1000);

  RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // enable PORTB
  set_GPIO(GPIOB, 5, 0b0010);         // CNF=0, MODE=2 (2MHz output) PB5 LED

  RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // enable PORTC
  set_GPIO(GPIOC,  8, 0b0010);        // CNF=0, MODE=2 (2MHz output) PC8,PC9 LEDs
  set_GPIO(GPIOC,  9, 0b0010);
  set_GPIO(GPIOC, 10, 0b0010);        // row output for reading buttons
  GPIOC->ODR |= (1<<10);              // 1 when button pushed

  RCC->APB2ENR |= RCC_APB2ENR_IOPDEN; // enable PORTD

  RCC->APB2ENR |= RCC_APB2ENR_TIM1EN; // enable Timer1

  SysTick->LOAD = F_CPU/1000-1;       // 1ms tick (-1 rule!)
  SysTick->CTRL = 0b111;              // internal clock, enable timer and interrupt

  lcd_init();
  lcd_display(0, 0, "  Cntr1:");      // only need to write these once
  lcd_display(0, 1, "Cntr100:");

  u16 cntr1_dl = make_deadline(0);
  u16 cntr100_dl = make_deadline(0);
  u16 button_dl = make_deadline(0);
  u16 LCD_dl = make_deadline(0);
  u8  LCD_flag = 0;

  u16 cntr1 = 0;
  u16 cntr100 = 0;

  u8 old_b1 = 0;
  u8 old_b100 = 0;

  while (1)
  {
    if (deadline_reached(cntr1_dl))
    {
      GPIOC->ODR ^= (1<<9);  // LED
      cntr1++;
      cntr1_dl += 1000;
   }
    if (deadline_reached(cntr100_dl))
    {
      GPIOC->ODR ^= (1<<8);  // LED
      cntr100++;
      cntr100_dl += 100;
    }
    if (deadline_reached(button_dl))
    {
      u8 new_b = (GPIOA->IDR & (1<<11)) ? 1 : 0;
      if (new_b && !old_b1)
      {
        cntr1 = 0;
        LCD_flag = 1;
        LCD_dl = make_deadline(400);
      }
      old_b1 = new_b;

      new_b = (GPIOA->IDR & (1<<12)) ? 1 : 0;
      if (new_b && !old_b100)
      {
        cntr100 = 0;
        LCD_flag = 1;
        LCD_dl = make_deadline(400);
      }
      old_b100 = new_b;

      GPIOB->ODR ^= (1<<5);  // LED
      button_dl += 50;
    }
    if (LCD_flag || deadline_reached(LCD_dl))
    {
      char buf[LCD_WIDTH+1];

      sprintf(buf, "%5u", cntr1);
      lcd_display(9, 0, buf);
      sprintf(buf, "%5u", cntr100);
      lcd_display(9, 1, buf);

      GPIOA->ODR ^= (1<<8);  // LED

      if (LCD_flag)
      {
        LCD_flag = 0;
        LCD_dl = make_deadline(400);
      }
      else
        LCD_dl += 400;
    }
  }
}

Notice that we look for the leading edge of two different buttons (new_b && !old_b is only true when the button has just gone from 0 to 1), with each button clearing one of the two counters.  Any time a counter is cleared, we also set the LCD_flag to force an immediate display update, and recompute a new full-length deadline for the counter. Good user interface design requires that a button or key push be followed very quickly with some form of visual or audible feedback - here this feedback is the cleared counter value being immediately displayed.  Also note how the 4x4 button matrix (which we haven't talked about yet) must be configured to enable the two buttons.  PC10 sets the top button row high, and PA11 / PA12 are set as inputs with pulldowns, so they will stay low (because of the pulldowns) unless one of the top two rightmost buttons is pushed, in which case that input will go high.  This will become more clear after the next tutorial chapter is published, which will deal with the button matrix.  For now it's enough to know that PA11 and PA12 are set as inputs that remain low until their associated buttons are pushed, at which time that input goes high.

Also note that each task, when active, toggles a different LED.  This is just to give a visual cue as to when the tasks are going active.

Finally, observe that the 1 second count is not smooth, but seems to go long-short-long-short...  This is an artifact of the 400ms display update rate, which is not a submultiple of 1 second (500ms or 250ms would be submultiples).  This results in one value of the count being displayed for 2 LCD updates (800ms) and the next value being displayed for 3 LCD updates (1200ms).  If something like this is a problem, you can either choose an LCD update rate that is a submultiple of the time period in question, or you can set the LCD_flag for an immediate update every time the 1-second counter increments.  I suggest you try both variations just to see the differences in behavior.

Here's a short video of the example program in action:

Next

We'll take a look at this mysterious 4x4 button matrix and how to read it, and take a look at some other hardware as well.


Previous post by Mike Silva:
   Introduction to Microcontrollers - Adding Some Real-World Hardware
Next post by Mike Silva:
   Introduction to Microcontrollers - Buttons and Bouncing


Comments:

[ - ]
Comment by martin48October 8, 2014
You can also use anew ^ aold to see which bits have changed. Then test if anew is high for a leading edge and otherwise falking. Also, why not use a macro for timeout_expired?

To post reply to a comment, click on the 'reply' button attached to each comment. To post a new comment (not a reply to a comment) check out the 'Write a Comment' tab at the top of the comments.

Registering will allow you to participate to the forums on ALL the related sites and give you access to all pdf downloads.

Sign up
or Sign in