Skip to content

The project is built for the purposes of biochemistry lab technicians - some example use cases are outlined below. This naturally leads to this mapping of user needs, resulting in the below diagram, where we decide the essential features.

Notifications You must be signed in to change notification settings

KennyLaw02/ConcurrentLabTimer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

The M Timer Project Developer Guide

Introduction

We designed a custom laboratory timer using a UNSW COAST board (with a NUCELO-F303RE board). We used an STM32 ARM microcontroller with the HAL C library to accomplish this. There is both a standard clock mode, with an alarm, stopwatch, countdown timer, and date/time, as well as a timer mode that allows for 4 concurrent timers to be run at once.


Background

The project is built for the purposes of biochemistry lab technicians - some example use cases are outlined below. This naturally leads to this mapping of user needs, resulting in the below diagram, where we decide the essential features.

These features can be grouped into the more general standard clock features (Date/time, Alarms, stopwatch, sequential timers), and lab timer features (multiple parallel timers)


Quick Reference

Here are the basic steps to get set up and flash the project onto the board.

  1. Install STM32CubeIDE - you need to sign up to download the installer
  2. Open the project from your file system

  1. Import the project

  1. Press the run button with the USB connected to your computer and board

System Overview

The M Timer is designed to manage various functionalities such as clocks, alarms, down timers, and lab timers. It integrates multiple peripherals, including an LCD display, buzzer, LEDs, and a keypad.

The system is designed such that time insensitive components are put on the main loop, whilst things that require more precision are done via interrupts. Time insensitive components include the keypad and lcd, and time sensitive components include starting and stopping the stopwatch via switches.

Project IOC

The project's IOC contains several settings to automate the creation of code to initialize the various hardware components such as the RTC, Timer interrupts, PWM and EXTI.


Subsystems and Their Integration

Hardware components & Interrupts

Real-Time Clock (RTC)

The real time clock is a hardware component on the board that ticks a preset date and time. The purpose of the component is to independently tick a set date and time without requiring CPU cycles. The CPU only interacts with the RTC during retrieving or setting the dates and times.

The RTC hardware is also able to generate an alarm interrupt by setting an alarm. The check on whether the alarm should trigger or not is also done by the RTC component without requiring CPU cycles. The CPU only interacts with the alarm system when setting an alarm or an alarm interrupt was generated.

Timer interrupts

Timer interrupts are periodically generated (in this software's case, every second and millisecond) to run periodic routines such as ticking timers.

PWM generator

The PWM generator is used solely for the buzzer by providing a square wave signal in which its frequency and duty cycles can be set by the software at will.

External Interrupts (EXTI)

External interrupts are used to retrieve inputs for time sensitive tasks. The software mainly uses EXTIs with Switches 1 and 2 to maintain precision in time sensitive functions such as the stopwatch for start/stop and resetting.

UART DMA interrupts

Whenever a HAL_UART_Receive_DMA() is called, the UART sleeps until a user input has been detected, in which a UART interrupt is generated. This allows the software to not busy wait for user inputs and run other functions while UART communications are handled in the background.

UI

The diagram offers a quick overview of the board and the components it includes. The blue button seen in the top-middle portion of the board for example, transitions between the lab timer mode and the standard clock mode.


Design Approaches and Decisions

Input Validation

Concerning input validation, take note that apart from UART inputs, the available switches and keypad buttons means that users will not be able to give inputs that hardware buttons aren't linked to in software. Inputs are validated with the following methods

  • In the main loop, a keypad input is recorded at the start. Then the focused function is displayed (displayClock(), displayStopwatch(), displayLabTimers(), etc. ). Inside this display function, the keypad input is checked on whether it corresponds to an action, otherwise the function continues, display the current state, and return to the main loop.
  • For times and dates, the functions, isValidTime() and isValidDate() in clock.c, which ensure times are within the 24h time format, and that dates are valid for the Gregorian calendar.
  • For inputs to the timer, the function isValidTimerInput() within labTimers.c, is used to check that the newTime string is parsed correctly, and that each time field is within a valid range (minutes < 60, seconds < 60, hours within 99)

Handling time format conversion

Below are a few examples of how formatting between strings and integers are handled.

It is preferential to store everything in seconds in the .currTime and configTime fields of Timer-like structs, and convert these into sec/min/hr when required. The only exception to this is rule is the stopwatch where it stores its time in milliseconds. You can see more explanations of this in the library deep dive further down.

Extracting the time from the RTC

void getAlarm(char *time) {
	// Get RTC alarm
	RTC_AlarmTypeDef currAlarm = {0};
	HAL_RTC_GetAlarm(&hrtc, &currAlarm, RTC_ALARM_A, RTC_FORMAT_BIN);

	// Extract time fields and format
	sprintf(time, "%02d:%02d:%02d %cM",
			currAlarm.AlarmTime.Hours,
			currAlarm.AlarmTime.Minutes,
			currAlarm.AlarmTime.Seconds,
			currAlarm.AlarmTime.TimeFormat == RTC_HOURFORMAT12_AM ? 'A' : 'P');
}

Parsing a string into respective time variables

	// Parse time string to alarm struct
	int hour, min, sec;
	sscanf(time, "%d:%d:%d", &hour, &min, &sec); // Scanning the time string in
	newAlarm.AlarmTime.Hours = (uint8_t) hour;
	newAlarm.AlarmTime.Minutes = (uint8_t) min;
	newAlarm.AlarmTime.Seconds = (uint8_t) sec;

Formatting integers to strings (from getDownTimer)

	int hours, minutes, seconds;
	int totalSec = countDownTimer.currTime;
	hours = totalSec / (60 * 60);
	totalSec %= 60 * 60; // Using modulo to breakdown the time properly
	minutes = totalSec / 60;
	seconds = totalSec % 60;

	// Format to string
	sprintf(time, "%02d:%02d:%02d", hours, minutes, seconds);

General code flow

The Main body loop

Subsequently, diagrams are used here to give you an understanding of how the main loop operates / how each of the subsystem loops work.

Overview diagram


Standard clock mode flow

Timer Queue / Sequential Timers

The diagram below is a visual of how the timer queue interacts with the rest of the system.

The diagram below shows the how the timer ticks when a timer interrupt is generated.

Callbacks & Interrupts Overview

HAL_GPIO_EXTI_Callback

This function handles GPIO (General Purpose Input/Output) pin interrupts. Below is an explanation of the key conditions and actions taken:

  • Standard Clock Mode (STDCLOCKMODE)

    • SW1_PIN Pressed
      • If the alarm is ringing (alarmStatus == ALARM_TRIGGERED), the alarm is snoozed (snoozeAlarm).
      • If the countdown timer is ringing (downTimerStatus == DWN_TRIGGERED), the countdown timer is snoozed (snoozeDownTimer).
      • If the timer queue is ringing (TQ_Status == TQ_TRIGGERED), the timer queue is snoozed (snoozeTimerQueue).
      • If the stopwatch is focused (currClockFocus == STOPWATCHFOCUS), the stopwatch is started or stopped depending on its current state (setStopwatchMode).
    • SW2_PIN Pressed
      • If the stopwatch is focused (currClockFocus == STOPWATCHFOCUS), the stopwatch is reset (resetStopwatch).
  • Lab Timer Mode (LABTIMERMODE)

    • SW1_PIN Pressed
      • If the lab timer is triggered (timerStateManager.timerState == LBT_TRIGGERED), the lab timer is snoozed (snoozeLabTimer()). All switch presses are ignored for the next four presses (switchesPressed).

HAL_TIM_PeriodElapsedCallback

This function handles timer interrupts. It processes different actions based on whether the interrupt was triggered by htim6 or htim7, two of the timers we use in our program

  • htim6 (Triggers every millisecond) -> stopwatch requires more precision

    • Ticks the stopwatch (tickStopwatch(&watch)).
    • Ticks the DxLEDs (DxLED_tick()).
    • Ticks the buzzer (buzzer_tick()).
  • htim7 (Triggers every second) -> other timers require less precision

    • Ticks the countdown timer (tickDownTimer()) which is now deprecated.
    • Ticks the timer queue (tickTimerQueue()).
    • Ticks the lab timer (tickLabTimer()).

HAL_RTC_AlarmAEventCallback

This function handles RTC (Real-Time Clock) alarm interrupts. When an alarm interrupt occurs, the current mode is temporarily set to STDCLOCKMODE to handle the alarm:

  • The current mode is saved (deviceMode oldMode = currMode).
  • The mode is set to STDCLOCKMODE.
  • The alarm status is updated to indicate the alarm has triggered (alarmStatus = ALARM_TRIGGERED).
  • The DxLEDs (D1 to D4) are set to blink (DxLED_set(DxLED, LED_BLINK)).
  • The original mode is restored (currMode = oldMode).
    • The reason for switching modes is to ensure that when user switched to lab timer mode, DxLED sets the LED to operate only the blinking in the correct mode as it uses the currMode global variable to determine which mode the function call came from.

HAL_UART_RxCpltCallback

This callback function is primarily used for capturing user input when the user enters set mode for the sequential timers function. Support for future features involving the UART can be achieved by adding encapsulating the existing timer queue instructions in an if statement, checking that the sequential timer is focused. Else if statements can then be used for control flow to perform the appropriate function's UART logic.

States transitions and control logic of various functions

The state diagrams below show the state transitions for both sequential and lab timers, the stopwatch and alarm.

Additionally, the following flow diagrams detail the logic of each function when focused or their interrupt was triggered.

Clock

Alarms

Stopwatch

Lab Timer mode flow


The LabTimer struct

The most important struct in the lab timer library. The 4 lab timers in the library comprise of a static array of 4 of these structs. The code definition for the struct is as follows.

typedef struct {
    LabTimerMode mode;           // Is the timer in display mode or edit mode? 
    int currTime;                // Time left in seconds
    int configTime;              // The time configured in seconds
    int isLabTimerOn;	         // Whether the timer is currently running
    int newLabTimerHr;	         // The Hr/Min/Sec during editing 
    int newLabTimerMin;
    int newLabTimerSec;
    char labTimerInput[3];       // The two digit input for min/sec/hour when users are editing times
    int labTimerDigitSel;        // Which digit is selecte currently
    ClkEditSelect labEditSelect; 
    LabTimerState state;         // State of the timer - is it running? paused? going off?
    char labTimerName[11];       // User defined label for the timer 
} LabTimer;

Values are initialized to 0 apart from the newLabTimerHr/Min/Sec, which is set to -1, as this value is used to indicate the user left that field untouched.

Global variables explained

We use global variables to act as states across subsystems, that are defined in main.h. We need to know the following things.

typedef enum {
	STDCLOCKMODE,
	LABTIMERMODE
} deviceMode;
extern deviceMode currMode;

The deviceMode is the current state of the device - either in the standard clock state or the lab timer mode. We need to know this because alarms that go off in one mode should not go off in another - and neither should accomanying LEDs, Progress bars, or Buzzer alerts

typedef enum {
	ALARM_IDLE,
	ALARM_TRIGGERED,
	ALARM_SNOOZED
} AlarmState;

The AlarmState is the current state of the alarm - this is checked in the main loop, and if it is set to triggered, then we set off an alert with the LCD and buzzer, then set the status back to idle. This is a code snippit of the alarm check in the main while loop that specifically checks whether the alarm was set to triggered by an interupt.

if (alarmStatus == ALARM_TRIGGERED) {
				lcd_writeStringTrailing("Alarm Triggered!"); // Write to the LCD
				lcd_setCursor(1, 0); // Set cursor to next line
				lcd_writeStringTrailing("SW1 to snooze"); // Write to LCD again
				buzzer_play(262, BUZZER_BEEPING); // Play a beeping noise 
				// Skip remaining iterations in main loop, ignoring inputs
				continue;
			// If alarm has been snoozed, clean up LCD
			// and put back to idle
} else if (alarmStatus == ALARM_SNOOZED) {
  buzzer_mute(); // Mute the alarm 
  lcd_clear(); // Clear alarm popup on screen
  alarmStatus = ALARM_IDLE; // Set the alarm back to an idle state, until user starts it again
}

Following this logic, we have states for the downtimer (now deprecated and replaced by the sequential timerss), Sequential Timers, and labTimers.

The LabTimerFocus enum shows which lab timer the user is currently on, and this enum corresponds to the index of the labTimer in the labTimers list (defined in labTimers.c)

Deep Dive through the Libraries

All libraries extensively interact with the Keypad and LCD libraries through their functions so we discuss those first. All init() commands are related to setting operating modes for pins & setting their default states.


keypad.h

We provide a 2d array of the keypad layout, defined by KEYPAD_LAYOUT, in order to make code easy to understand. Changing the press delays may cause issues with ghost inputs, so leave at 200ms unless changes are strictly required. Features of library is as follows.

  • getKeypadInput() - The only function the library provides, which returns the character that has been pressed by the user. Returns \0 when no key has been pressed.

This function is ran at the beginning of the main loop, to continuously check for user input.


lcd.h

Arguably the most important library as this is the main 'O' part of the user IO. The library is well commented, and it is important to note that the display is set to 4 bit mode, so all commands are sent using only the lower or upper nibbles (4 bits).

The LCD requires a delay of 40ms (we set to 50ms for a safety net) to stabilize - do not remove this from the code.

  • lcd_sendCmd()
    • First transmits upper then lower nibble using command lcd_cmd4
  • lcd_cmd4()
    • Sets up data pins then pulses the enable pin to latch the data
  • lcd_clear() & lcd_clearLine()
    • Clears the entire display/current line on LCD
  • lcd_writechar() & lcd_writeString()
    • Writes a character or string respectively to the current position of the cursor. lcd_writeStringTrailing() fills rest of line with trailing space to clear pre-existing text.
  • lcd_setCursor() & lcd_toggleCursor()
    • Sets cursor position and toggles the cursor modes (on/off, blinking or not blinking)

shiftReg.h

This library is designed to interface with a shift register, enabling control of the 16 LED lights at the bottom of the board. This is used for the progress bar feature.

  • shiftReg_init()
    • Initializes the GPIO pins connected to the shift register and sets their initial states. It also clears any previous data by calling shiftReg_display(0).
  • shiftReg_display()
    • Takes a 16-bit number and serially feeds it to the shift register: 0 is off and 1 is on for an LED.

clock.h

This defines the interface for a clock system, including time management, alarms, countdown timers, and sequential timerss. The code is documented extensively, so only a quick rundown of features will be provided here.

The following structs and enums will be detailed below:

  • General date/time
  • Alarm
  • Sequential timers
  • Stopwatch (will be covered in stopwatch.h/stopwatch.c sections)

Note that the countdown timer is deprecated and was replaced by the timer queue.

General Date & Time

This is used to set and get the hardware RTC (real time clock)

  • ClockMode enum
    • Used to track the current state of the clock, in edit or display mode
  • ClkEditSelect enum:
    • Used to manage cursor position when editing the times
  • setDateTime() & getDateTime():
    • Sets & gets hardware RTC and parses into 12-hr format with dd-mm-yyyy date format using the following functions:
      • displayClock()
      • getSelectStr()
      • getDateTimeInput()
      • convert24to12()
  • isValidTime() & isValidDate()
    • Checks for user input validity

Alarm

When the alarm is triggered, it is caught in the main loop via the alarmStatus variable which is set to the alarm being triggered.

Note that we use the supplied hardware alarm in the real time clock, so we use provided functions/types such as RTC_AlarmTypeDef to create and manage an alarm.

  • AlarmMode enum: Used to track current state of alarm, in edit or display mode
  • displayAlarm(): Displays alarm
    • if '0' is pressed as input then we deactivate/set the alarm again with the HAL_RTC_DeactivateAlarm function. There isn't a set function so we just create the alarm again
    • Edit modes are similar to those previously discussed
  • getAlarmInput()
    • Creates a new alarm based on user input. We cycle between 1st and 2nd digit for each field (sec/min/hr)
  • getAlarm()
    • Creates an alarm given a time in a string - read the 'handling time conversion formatting' section for further details
  • setAlarm()
    • Creates a new alarm using the RTC (real time clock). We use internal alarm A, which must be configured as follows in the IOC (input/output configuration interface). Note the hour format is set to 12h time, and we have conversions for AM/PM
  • SnoozeAlarm()
    • Disables the LEDs using the Dx_LEDs library so they stop flashing, then update the alarm status.

Timer Queue

The timer queue runs off an array of TimerQueueNodes, defined as follows

typedef struct {
	TimerQueueMode mode; // Either inactive, idle, running, or being edited
	int currTime; // Time left in seconds
	int configTime; // Configured time in seconds
} TimerQueueNode;
  • displayTimerQueue()
    • Displays the sequential timer dashboard
    • Pressing 'C' again will enter set mode, in which configuring the timers will then be done on via UART communication.
  • getTimerQueue()
    • Retrieves the current timer's time in mm:ss format
  • setTimerQueue()
    • Given a string of space separated times and the number of new timers, configures the timer queue to the new set of times.
  • snoozeTimerQueue()
    • Disables the notification of the timer queue being complete and resets the timer queue triggered signal.
  • tickTimerQueue()
    • Ticks the active timer and is placed in the htim7 interrupt that triggers every second.
  • isValidSeqTime()
    • Goes through the space separated set of timers to check if the given times are a valid input to use.

On device startup, the timers are all set to inactive at the start. The index of the currently tracked timer is done through the currentTimerIdx variable, which is set to 0 (the first timer in the queue) at startup. The standard clock mode flow's timer queue flow diagram to understand how the timer queue provides a visualisation of the control logic of the timers. The diagram below presents an example of a timer queue of 3, and the timer queue states each timer goes through.

pins.h

This library simply lists the pin connections of each I/O and hardware component. It is used as a convenient library to quickly reference the appropriate ports and pins without having to look at the board's pin connections data sheet.


config.h

This library is easily open to expandability for future user configuration settings. Its current purpose is to provide a simple interface for giving the lab timers custom names.

// CONFIGURE LAB TIMER NAMES
// A timer name must not exceed this limit
// If the character limit is exceeded or an empty string is given
// a default name will be used instead
#define LABTIMERNAME_MAX 10

#define LABTIMERNAME_1 "Custom 1"
#define LABTIMERNAME_2 "Enzyme 2"
#define LABTIMERNAME_3 "ElectroPho"
#define LABTIMERNAME_4 "12345678910"

buzzer.h

The buzzer library is a conveniently simplified interface to abstract the complexities of PWM signal generation as well as computing sound frequencies to its corresponding ARR and PSC values.

  • buzzer_play(int freq, BuzzerMode mode)
    • Sets the buzzer to play at the given frequency and whether it should buzz in beeps or continuously.
  • buzzer_mute() / buzzer_unmute()
    • Sets the buzzer to stop/resume playing sounds
  • buzzer_tick()
    • Used in timer interrupts to tick the buzzer's internal counter to track the durations of its beeps.

Dx_LEDs.h

The LEDs library is used for LEDs D1 - D4 and is able to also detect which mode is currently being run to appropriately display the LEDs when needed. E.g. If the LEDs were set to blinking in clock mode, they won't blink in lab timer mode.

  • DxLED_set(DxSel selected, DxLEDMode newMode)

    • Sets an LED (DxSel selected) to operate in a new mode, such as on, flashing and off.
    • It will set the given LED to the new mode for the current operating mode. E.g. If function was called during lab timer mode, the LED's mode will only display when in lab timer mode, not clock mode.
  • DxLED_tick()

    • Ticks the internal counters of each LED to track the durations of its blinks.

labTimers.h

This library contains all functions relating to the lab timer mode. The code takes on a nearly identical approach to the countdown timer (deprecated) and sequential timer.

  • setTimerModeFocus()
    • Checks if the user has pressed any of the keys 'A'-'D' and switches focus to the other lab timer if necessary.
  • getLabTimerInput()
    • Used during set mode to configure the time fields with the new time the user gave.
  • getLabTimer()
    • Retrieves the lab timer's elapsed time in hh:mm:ss format
  • setLabTimer()
    • Sets the given lab timer to the new time given in the string.
  • snoozeLabTimer()
    • Stops the notifications displaying that the lab timer triggered and resets any lab timer triggered signals.
  • tickLabTimer()
    • Used in htim7 interrupts to tick the lab timers every second.
  • isValidTimerInput()
    • Checks that the given new time string is a valid input.
  • getProgressVal()
    • A helper function for the progress bar feature. Used to calculate a 16-bit value of 1's and 0's that would illustrate the progress of the timer. E.g. 0x00FF would indicate the timer is 50% complete.

stopwatch.h

The stopwatch library is used to operate the stopwatch and is responsible for displaying, starting, stopping and resetting the function.

  • newStopwatch()
    • Used during initialisation phase to setup a stopwatch instance.
  • setStopwatchMode()
    • Used to set the stopwatch to either be running or idle.
  • readStopwatchMode()
    • Used to get the mode the stopwatch is currently at.
  • tickStopwatch()
    • Used in htim6 and ticks the stopwatch every millisecond.
  • resetStopwatch()
    • Resets the stopwatch's elapsed time back to 0 and sets it to idle mode.
  • readStopwatch()
    • Returns the time elapsed in milliseconds.
  • formatStopwatchTime()
    • Writes to the given string the stopwatch's elapsed in in the format hh:mm:ss.sss
  • displayStopWatch()
    • Displays the stopwatch dashboard on the LCD display.

Further contributions

This project was created from a 3-person team using UNSW CSE gitlab for version control. These are the steps for contributing changes

  1. Clone the repository to your local disk from gitlab
  2. Create a new branch from the current main branch
  3. Commit your changes and ensure they work via technical validation, before committing them with a descriptive and short commit message.
  4. Push the commits to the branch
  5. Submit a merge request to main if new feature is complete.

About

The project is built for the purposes of biochemistry lab technicians - some example use cases are outlined below. This naturally leads to this mapping of user needs, resulting in the below diagram, where we decide the essential features.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages