An autonomous clinical assistance robot written entirely in ARM Assembly (Thumb-2) targeting the STM32F401RC microcontroller. Designed to navigate hospital corridors, monitor patient vitals, sanitise hands, and dispense medications β programmed at the bare-metal register level with no HAL, CMSIS, or OS libraries.
Microprocessors Final Project β Cairo University, Faculty of Engineering, Computer Engineering Department
| Nour Ibrahim | Youmna Mohamed |
| Yehia Mahmoud | Yasmine Ismail |
| Yassin Abdelatty | Mostafa El Shazly |
| Moaz Amr | Anton Azer |
| Maryam Gamal | Hashem Mohamed |
βΆ Watch Robo in Action β Full Demo on YouTube
(Replace the link above with the actual YouTube video URL once uploaded)
- About the Project
- System Architecture & Keypad Mapping
- Hardware Architecture & Circuit Components
- Software Directory Structure
- Getting Started & Building
- Interrupt Map
- Core Code Architecture
- π¬ Driver Deep Dives
- π File-by-File Technical Deep Dive
- π¬ Core Feature Specifications & File Collaborations
- 1. Heart Rate & SpOβ Monitor
- 2. Breathing Waveform Monitor
- 3. Volatility Stress Index
- 4. Sub-Dermal Vein Finder
- 5. Body Temperature Monitor
- 6. Servo Medication Dispenser
- 7. Environmental Smoke Alert
- 8. IR Hand Sanitizer
- 9. Landolt C Vision Test
- 10. Autonomous Line-Tracking Guidance
- 11. Mobile App Bluetooth Override
- 12. Bedside IR Station Call-Docking Feature
- π± Mobile App Control
- π· Visual Walkthrough & Simulation
- π Acknowledgements
In medical wards, reducing contact between clinical staff and infectious patients is critical. The Nursing Assistant Robot is a modular, low-cost autonomous assistant that navigates wards, delivers scheduled medications, detects fires, and checks patient diagnostics.
This project is a bare-metal engineering endeavor written entirely in ARM Assembly. Every operation β from software I2C bit-banging to high-speed SPI display transmissions and multi-channel analog filtration β is done by directly manipulating registers. No HAL, no CMSIS-Drivers, no C libraries.
The user interface is driven by an IR Remote Control mapped to a central state machine. Pressing key numbers triggers hardware-level interrupts that transition the robot between screens and operational routines. Key 0 is an independent selection key that directs users to the sub-feature menu.
Home / Main Menu (Default)
β
ββββββββββββββββ¬ββββββββββββββββββΌβββββββββββββββ¬βββββββββββββββ¬βββββββββββββββ
β (Key 1) β (Key 2) β (Key 3) β (Key 4) β (Key 5) β (Key 0)
βΌ βΌ βΌ βΌ βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββ βββββββββββββ βββββββββββββ βββββββββββββ
β Sanitise β β Heart β β Breathing β β Meds β β Body β β More Menu β
β Routine β β Diagnosticsβ β Waveform β β Timer β β Temp β βββββββ¬ββββββ
βββββββββββββ βββββββββββββ βββββββββββββ βββββββ¬ββββββ βββββββββββββ β
β β
βΌ (Trigger) β (Keys 6, 7, 8)
βββββββββββββ βΌ
β Med Alert β βββββββββββββ
β & Dispenseβ β Sub-Menu β
βββββββββββββ β features β
βββββββββββββ
Key Mappings:
| Key | Feature | Pins Involved |
|---|---|---|
| 1 | Hand Sanitizer | PA4 (proximity), PA5 (relay pump) |
| 2 | Heart Rate & SpOβ | PB8/PB9 I2C β MAX30102 |
| 3 | Breathing Waveform | PA0 β ADC CH0 |
| 4 | Medication Timer + Servo | PA6 (TIM3 CH1 servo) |
| 5 | Body Temperature | PB8/PB9 I2C β MAX30102 |
| 0 | More Menu | β |
| 6 | Landolt C Eye Test | TFT display |
| 7 | Vein Finder | PA7 β ADC CH7 |
| 8 | Stress Index | g_bpm calculation |
| C/# | Return to Home | β |
Background Routines (always running):
- Line Tracking: 3-bit IR reflective array on
PB12βPB14for autonomous navigation. - Bluetooth Override: Commands from the Robo Mobile App via USART2 (
PA2/PA3) override autonomous movement. - Bedside IR Stations: IR beacons at bedsides or charging stations halt the robot for clinical care delivery.
[3x Li-ion Batteries (11.1V)] ββββΊ [Buck Converter (5V Output)]
β
ββββββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββ
βΌ βΌ
[STM32F401RC MCU Core] [Actuators & Sensors]
ββ SPI1 Bus ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ ILI9341 3.2" TFT Display
ββ I2C1 Bus ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ MAX30102 Pulse Oximeter
ββ USART2 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ HC-05 Bluetooth Module
ββ ADC1 (PA0/PA1/PA7) ββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ MQ-2, Breathing, Vein Sensors
ββ GPIO Inputs (PA4, PB10, PB12-15) ββββββββββββββββββββββββββββββββββββββββΊ Sanitizer, IR Remote, Line Trackers
ββ GPIO Outputs (PA5, PB4, PA8-11) βββββββββββββββββββββββββββββββββββββββββΊ Pump Relay, Buzzer, DC Motors
ββ TIM3 PWM (PA6) ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ Positional Medicine Servo
ββ TIM4 PWM (PB6/PB7) ββββββββββββββββββββββββββββββββββββββββββββββββββββββΊ DC Motor Speed Control
ββ TIM2 + EXTI10 (PB10) ββββββββββββββββββββββββββββββββββββββββββββββββββββΊ IR Remote Decode
Component List:
- Chassis & Motion: 6-wheel drive chassis, 6 TT DC Motors driven by two motor drivers, 1 line tracker sensor board (3 IR sensors on
PB12βPB14). - Core MCU: STM32F401RC (ARM Cortex-M4, 16 MHz), ST-Link V2 SWD debugger, 3x 18650 Li-ion batteries (11.1V), 5V step-down buck converter.
- Display & UI: ILI9341 3.2" Color TFT LCD over SPI1, piezo buzzer on
PB4, IR receiver onPB10+ IR remote. - Actuators: SG90/MG995 servo on
PA6(TIM3 CH1) for medicine dispensing, mini submersible 5V water pump via relay onPA5. - Sensors: MAX30102 pulse oximeter on I2C1 (
PB8/PB9), MQ-2 gas sensor onPA1(ADC CH1), HC-SR04 sonar onPC15(Trig) /PC14(Echo), sanitizer proximity IR onPA4, bedside IR alignment receiver. - Discrete Components: Safety diodes, push-buttons, IR LEDs, pull-down resistors, 74HC logic latches, decoders, battery holder, 3 breadboards, and jumper wires.
project/
β
βββ README.md
βββ motion.s # Guidance control loop + motor driving vectors
β
βββ core/
β βββ main.s # Entry point, SysTick 1ms & super-loop scheduler
β βββ constants.s # Hardware register offsets, state IDs, bit masks
β βββ global.s # Shared RAM variable allocations
β βββ gpio.s # GPIO clock/mode configuration library
β βββ motion_constants.s # Motor velocities and BT speed parameters
β βββ ui_state.s # State-machine dispatcher & keypad transitions
β
βββ features/
β βββ max.s # I2C driver + high-pass filters for MAX30102
β βββ breathing.s # ADC breathing sampler + baseline drift tracking
β βββ stress.s # Live heart-rate volatility analyzer
β βββ vein.s # ADC signal averager + state-aware buzzer feedback
β βββ medicine.s # Background medication timers + servo rotation
β βββ smoke.s # MQ-2 safety gates, warmup, and alarms
β βββ santizing.s # Hand proximity detection + relay sequencer
β βββ motion_bt.s # Bluetooth driving command parser
β βββ ultrasonic.s # Obstacle range calculations (HC-SR04)
β βββ ir_stations.s # Charging/medical station alignment beacons
β
βββ Drivers/
βββ adc.s # ADC1 analog controller + Analog Watchdog (AWD)
βββ i2c.s # I2C1 hardware master protocol driver
βββ pwm.s # TIM3/TIM4 PWM for servo and motor speed
βββ bluetooth.s # USART2 peripheral + interrupts + queues
βββ bluetooth_buffer.s # Non-blocking RAM ring buffers (Rx/Tx)
βββ buzzer.s # Periodic alert beeper on PB4
βββ tft_low.s # SPI1 ILI9341 register-level screen driver
βββ tft_gfx.s # Graphics engine (shapes, waves, text rendering)
- Install Keil MDK-ARM v5.39+ and the Keil.STM32F4xx_DFP.2.17.1 device pack.
- Open the
.uvprojxworkspace file. - In Project β Options for Target β Output, enable "Create HEX File".
- Press F7 in Keil to compile the
.hexfile. - Open the Proteus schematic, double-click the STM32F401RC, set Clock to
16MHz, and point Program File to your.hex. - Press the green Play button to simulate.
- Connect ST-Link V2 to the SWD header (SWDIO, SWCLK, GND, 3.3V).
- In Keil: Options for Target β Debug β ST-Link Debugger.
- Press F8 to flash. Press hardware
RESETto boot.
The project relies on three hardware interrupts for time-critical operations. Everything else runs in the super-loop.
| Interrupt | Source | IRQ # | What it does |
|---|---|---|---|
EXTI15_10_IRQHandler |
Falling edge on PB10 (IR receiver) | IRQ 40 | Decodes NEC IR pulses using TIM2 timestamps. Each falling edge is timed and classified as a start frame, a 0 bit, or a 1 bit. After 32 bits are collected and checksummed, g_ir_raw_code is set. |
ADC_IRQHandler (AWD) |
ADC1 Analog Watchdog, CH1 (MQ-2) | IRQ 18 | Fires when the smoke sensor reading exceeds 3000 (out of 4095). Sets Smoke_Alert_Flag in g_alarm_flags, forcing the state machine to jump to STATE_SMOKE_ALERT on the next UI tick. |
SysTick_Handler |
SysTick core timer, 1ms reload | β | Increments g_ms_ticks every 1ms. Used throughout the codebase for non-blocking timeouts, draw-rate throttling (100ms), and medication countdown timers. |
Note: USART2 (Bluetooth) also uses interrupts internally to fill ring buffers without blocking the main loop.
Files: core/constants.s, core/global.s
Hardware register locations and shared variables are declared globally. Constants map addresses inside constants.s while variables are reserved within global.s to manage memory maps systematically.
;=============================================================================
; constants.s - Peripheral Addresses and UI Key Definitions
;=============================================================================
RCC_BASE EQU 0x40023800
GPIOA_BASE EQU 0x40020000
GPIOB_BASE EQU 0x40020400
; System states
STATE_MAIN_MENU EQU 0
STATE_SANITIZING EQU 1
STATE_HEART_RATE EQU 2
STATE_BREATHING EQU 3
STATE_VEIN_FINDER EQU 14
STATE_STRESS EQU 16
;=============================================================================
; global.s - RAM memory declarations
;=============================================================================
AREA VARIABLES, DATA, READWRITE
ALIGN
EXPORT g_sys_state
EXPORT g_bpm
EXPORT g_spo2
EXPORT g_ms_ticks
g_sys_state SPACE 4 ; Current active menu/feature state
g_bpm SPACE 4 ; Calculated beats-per-minute
g_spo2 SPACE 4 ; Blood oxygen saturation level
g_ms_ticks SPACE 4 ; Uptime clock in milliseconds
END- Address Mapping: Hardware registers are set using the base-plus-offset address pattern (e.g.,
RCC_BASE+RCC_AHB1ENRoffset). This decouples physical configurations from high-level logical assignments. - Access Scoping: Global system variables are declared inside
global.sinside theREADWRITEdata section, aligned to standard 32-bit boundaries. Access across compiler boundaries is granted by marking them asEXPORTat the source andIMPORTinside individual assemblies.
File: core/main.s
The main loop runs continuously after boot, coordinating non-blocking tasks.
INIT (Wakes up microcontroller)
β
βΌ
[ Main_InitGlobals ] <-- Reset RAM values to zero
β
βΌ
[ Main_InitCore ] <-- Set up SysTick (1ms) & load drivers
β
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββ
β MAIN LOOP (Continuous) β
β β
β 1. Check incoming Bluetooth serial stream β
β 2. Inject virtual keypad presses from BT β
β 3. Handle Bluetooth motion mode overrides β
β 4. Read & debounce IR Remote commands β
β 5. Run environmental fire check (MQ-2 ADC) β
β 6. Run Background Tasks (Meds, buzzer, etc.)β
β 7. Execute active state logic (vein, ppg) β
β 8. Check if 100ms has elapsed since draw β
β ββ YES: Trigger TFT UI Update screen β
β ββ NO: Skip draw step β
β 9. Run State Transition Cleanup routines β
ββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
Loops Forever
Main_Loop
BL BT_RxTask ; Check incoming Bluetooth serial stream
BL Main_ProcessBTKeyInjection ; Inject virtual keypad presses from BT
BL Main_ProcessBluetoothCmd ; Handle driving mode overrides
BL Main_CheckIRInput ; Debounce and check IR keypad inputs
BL Smoke_Check ; Read smoke sensor and check alarm flags
BL Main_BackgroundTasks ; Execute medicine timers & buzzer beeps
BL Main_DispatchByState ; Run active features (vein, ppg, breathing)
; Refresh the screen display every 100ms
LDR R0, =g_ms_ticks
LDR R1, [R0]
LDR R0, =ui_last_draw_tick
LDR R2, [R0]
SUBS R3, R1, R2
CMP R3, #100
BLO Main_SkipUI ; Skip if 100ms has not passed yet
Main_ForceUI
STR R1, [R0] ; Save current draw tick
BL UI_Update ; Render screens/graphs to TFT
Main_SkipUI
BL Main_HandleStateTransitions ; Clean up exiting states
B Main_Loop ; Repeat schedulerFile: core/ui_state.s
Directs page routing based on the state machine variables.
Alarms take absolute priority, with medication alarms locking down the display and smoke alarms applying a 5-second safety snooze cooldown (SMOKE_COOLDOWN_MS) between dismissals. During active medication programming, smoke interrupts are suppressed to prevent data loss.
Redrawing is split into two pathways:
- Transition Redraw: Triggered only when
g_sys_state != g_prev_state, runningTFT_Clear_Screenand drawing background layouts. - Partial Updates: Executed on every loop cycle to update numbers and waves without clearing the screen, preventing LCD flicker.
UI_Update FUNCTION
PUSH {R4, R5, LR}
; 1) Alarm Check: Force state change to MED_ALERT or SMOKE_ALERT
LDR R0, =g_alarm_flags
LDR R1, [R0]
TST R1, #Med_Alert_Flag
BNE.W Handle_Med_Alert
TST R1, #Smoke_Alert_Flag
BNE.W UI_Trigger_Smoke_Alert
UI_Handle_Input_Then_Route
BL UI_Handle_Input ; Check key codes and update active state
LDR R4, =g_sys_state
LDR R1, [R4]
LDR R5, =g_prev_state
LDR R0, [R5]
CMP R1, R0
BEQ.W UI_Partial_Update ; No state change -> perform partial refresh
; 2) State Changed: Clear screen and perform full redraw
STR R1, [R5]
MOV R4, R1
BL TFT_Clear_Screen
CMP R4, #STATE_MAIN_MENU
BEQ.W UI_Render_Main_Menu
CMP R4, #STATE_VEIN_FINDER
BEQ.W UI_Render_Vein
CMP R4, #STATE_STRESS
BEQ.W UI_Render_Stress
; ... Rest of the state comparisonsState Machine Dispatched Pages:
State Value (g_sys_state) |
Name Code | Page Description |
|---|---|---|
0 |
STATE_MAIN_MENU |
Home landing page showing operational modes. |
1 |
STATE_SANITIZING |
Hand sanitizer activation status page. |
2 |
STATE_HEART_RATE |
Numeric BPM and |
3 |
STATE_BREATHING |
Real-time respiratory graph plotting. |
4 |
STATE_MED_ALERT |
Flashing alert indicating dose is ready. |
6 |
STATE_MED_INPUT |
Input keypad console to program timer values. |
7 |
STATE_MED_DISPENSE |
Animates active dispenser servo rotations. |
8 |
STATE_SMOKE_ALERT |
Full-screen fire alarm siren. |
9 |
STATE_MED_WAITING |
Background countdown status check page. |
10 |
STATE_TEMP |
Body temperature display. |
11 |
STATE_PPG_WAVE |
Live raw PPG signal wave plotting. |
12 |
STATE_VISION |
Active Landolt C vision chart optotype test page. |
13 |
STATE_VISION_RES |
Vision exam scorecard calculation page. |
14 |
STATE_VEIN_FINDER |
Live vein mapper graph. |
16 |
STATE_STRESS |
Heart-rate volatility stress calculator page. |
17 |
STATE_MORE_MENU |
Sub-directory menu page. |
What this file does: Configures the STM32 ADC1 peripheral and provides a universal ADC_Read(channel) function. It also sets up a hardware Analog Watchdog that automatically triggers an interrupt if the smoke sensor goes dangerously high β no polling needed for fire detection.
Step-by-step initialization (ADC_Init):
- Enable GPIOA clock via
RCC_AHB1ENR(bit 0). - Enable ADC1 clock via
RCC_APB2ENR(bit 8). - Set
PA0andPA1to analog mode inGPIO_MODERβ both pin pairs get11bwhich disconnects the digital buffer entirely to prevent noise on analog pins. - Write
ADC_CR1: set resolution to 12-bit (clear bits [25:24]). - Write
ADC_CR2: select software trigger (clear [29:28]), right-align result (clear bit 11). - Write
ADC_SMPR2: assign 84 cycles sample time to CH0 and CH1, and a longer 480 cycles sample time for CH7 (the vein sensor needs more settling time due to the high-impedance IR sensor). - Set sequence length to 1 in
ADC_SQR1. - Power on: set
ADONinADC_CR2, then spin in a 1000-count stabilization delay before the ADC is considered ready.
Reading a sample (ADC_Read):
; 1. Write the channel number into SQR3[4:0]
; 2. Clear the EOC (End-Of-Conversion) flag in ADC_SR
; 3. Set SWSTART in ADC_CR2 to fire a software conversion
; 4. Poll EOC with a 100,000-count timeout watchdog
; 5. Read 12-bit result from ADC_DR, mask with 0xFFFTricky part β the timeout fail-safe: Without a timeout, if the ADC hardware ever locks up (e.g., glitch during simulation), the loop ADC_WaitEOC would spin forever, freezing the entire robot. The fix returns 4095 (max value) on timeout, which the smoke logic treats as "clean air" β the safer default.
Analog Watchdog (ADC_AWD_Init):
Configures the hardware to watch channel 1 (MQ-2) automatically. Sets HTR = 3000 (high threshold) and LTR = 0. In ADC_CR1, sets AWDSGL to watch a single channel, AWDEN to enable the watchdog, and AWDIE to fire an interrupt when the threshold is exceeded. Enables IRQ #18 in NVIC_ISER0. This means no polling is needed β the smoke alarm is purely interrupt-driven.
What this file does: Implements a full hardware I2C1 master on PB8 (SCL) and PB9 (SDA) to communicate with the MAX30102 pulse oximeter. Provides three entry points: single-byte write, single-byte read, and a 6-byte burst read (needed for reading the MAX30102 FIFO which holds 3 bytes of red + 3 bytes of IR in one go).
Initialization (I2C_Init):
- Enable GPIOB clock (
RCC_AHB1ENRbit 1) and I2C1 clock (RCC_APB1ENRbit 21). - Set
PB8/PB9MODER toAF(alternate function), OTYPER to open-drain (required by I2C spec β the line must be able to be pulled low by any device), OSPEEDR to high, PUPDR to pull-up. - Set AFRH for
PB8/PB9toAF4(I2C1 alternate function). - Software reset trick: Write
SWRSTtoI2C_CR1then clear it. This is critical β without it, a previously stuck bus state (e.g., after a bad transfer) will preventSTARTfrom being generated. This was a real bug encountered during development. - Write
I2C_CR2withFREQ = 42(tells the I2C peripheral the APB1 clock speed in MHz so it can calculate timing correctly). - Write
I2C_CCR = 210to set 100 kHz standard-mode clock speed:CCR = F_APB1 / (2 Γ F_SCL) = 42MHz / 200kHz = 210. - Write
I2C_TRISE = 43: maximum rise time in standard mode =(1000ns / (1/42MHz)) + 1 = 43. - Enable the peripheral:
I2C_CR1 = 0x0401(setsPE+ACK).
Writing a register (I2C_WriteReg):
The NEC I2C protocol requires: START β device address (write) β register address β data byte β STOP. Each step has a corresponding status flag in I2C_SR1 that must be polled before proceeding.
START β wait SB (Start Bit)
β write (device_addr << 1) β wait ADDR
β clear ADDR by reading SR1 then SR2
β wait TXE β write register address
β wait TXE β write data byte
β wait BTF (Byte Transfer Finished) β send STOP
Reading a register (I2C_ReadReg):
Reading over I2C requires two bus transactions: first write the register address (like a write, but without data), then issue a repeated START and switch to read mode with the address LSB = 1. The tricky part for single-byte reads is that ACK must be disabled and STOP must be queued BEFORE clearing the ADDR flag β if you do it after, the hardware has already started the next byte clock and you end up reading garbage.
6-byte burst read (I2C_Read6Bytes):
Used exclusively for MAX30102 FIFO data. Reads bytes 0β4 with ACK enabled, then on byte 4 atomically disables ACK and queues STOP so the final byte (byte 5) is NACKed correctly, signaling the device to release the bus.
Tricky part β I2C_WaitSR1_Set with timeout: Every wait in this driver goes through a shared helper that counts down from I2C_TIMEOUT = 60000. If any flag never arrives (e.g., device not connected), I2C_ForceStop is called to pull the bus to idle and the function returns an error code instead of freezing.
What this file does: Configures two hardware timers to produce PWM signals β TIM3 for the medicine servo and TIM4 for the DC motor speed controllers.
Initialization (PWM_Init):
- Enable GPIOA and GPIOB clocks, then TIM3 and TIM4 via
RCC_APB1ENR(bits 2 and 3). - Set
PA6to AF mode and assignAF2(TIM3 CH1) inGPIO_AFRL. Important: PA7 is intentionally left alone here because it's used by the ADC for the vein sensor β touching it would break analog readings. - Set
PB6/PB7to AF mode withAF2(TIM4 CH1/CH2) for motor PWM.
TIM3 β Servo at 50 Hz:
Prescaler = 15 β Timer clock = 16MHz / 16 = 1 MHz
ARR = 19999 β Period = 20000 Β΅s = 20 ms = 50 Hz
CCR1 range: 500 (0Β°) to 2500 (180Β°), center at 1500 (90Β°)
CCMR1 is set to 0x6868 which configures both CH1 and CH2 in PWM Mode 1 (output HIGH while counter < CCR, LOW after). CCER 0x0011 enables both channels' outputs.
TIM4 β DC Motors at 1 kHz:
Prescaler = 15 β Timer clock = 1 MHz
ARR = 999 β Period = 1000 Β΅s = 1 kHz
CCR range: 0 (stopped) to 999 (full speed)
PWM_Set_Motor_Speed(R0=left_speed, R1=right_speed): Clamps both values to max 999 before writing to CCR1/CCR2, preventing over-range writes that would break the PWM ratio.
PWM_Set_Servo_Pos(R0=pulse_us, R1=servo_select): Clamps pulse to [500, 2500] Β΅s. R1=0 writes to CCR1 (medicine servo on PA6), R1=1 writes to CCR2.
Tricky part: The TIM_EGR update event (STR #1, [R4, TIM_EGR]) must be triggered after writing PSC/ARR so the timer loads the new values immediately rather than waiting for the next overflow. Missing this causes the timer to run on old prescaler values until the first natural update, which can produce one incorrect PWM cycle.
What this file does: Initializes the ILI9341 3.2" display controller over SPI1 and provides the low-level primitives that the graphics engine (tft_gfx.s) builds on top of. This is the most latency-sensitive driver in the project β every pixel written goes through here.
Pin map:
PB0 β CS (Chip Select, active LOW)
PB1 β DC (Data/Command: LOW=command, HIGH=data)
PB2 β RST (Hardware reset, active LOW)
PB3 β SCK (SPI1 clock, AF5)
PB5 β MOSI (SPI1 data out, AF5)
GPIO + SPI setup (TFT_GPIO_SPI_Init):
- Enable GPIOB and SPI1 clocks.
- Set PB0/PB1/PB2 as push-pull outputs, PB3/PB5 as AF5 (SPI1). The OSPEEDR for all pins is set to maximum (0xFFβ¦) to support the high toggle rate needed for SPI.
- Idle state: CS=HIGH, DC=HIGH, RST=HIGH using a single BSRR write.
- Configure SPI1_CR1 =
0x035C:- Master mode, software NSS
- CPOL=0, CPHA=0 (SPI Mode 0 β ILI9341 requirement)
- Baud rate divider:
/4gives 4 MHz SPI clock from the 16 MHz APB2
Reset sequence (TFT_Reset): Pulses RST LOW for a short delay then HIGH β this is a hardware reset that brings the ILI9341 back to factory defaults before sending init commands.
Sending a byte (SPI_SendByte): The trickiest part. Two TXE checks are needed:
Wait TXE β write byte to SPI_DR β wait TXE again β wait BSY=0The reason for the second TXE wait: writing to DR starts the shift, but the byte isn't fully transmitted until both TXE is set again (shift register emptied into DR) AND BSY clears. Skipping the BSY check causes the next CS deassert to arrive while the last bit is still being clocked out, corrupting the final byte.
Command vs Data mode: The ILI9341 uses the DC pin to distinguish register commands from pixel data. TFT_BeginCommand pulls both CS and DC LOW, TFT_SwitchToData raises DC HIGH (keeping CS LOW), and TFT_EndTransaction raises CS HIGH to close the transfer.
Setting a pixel window (TFT_SetAddressWindow(x0, y0, x1, y1)):
Send CASET (0x2A) command + 4 data bytes: x0_high, x0_low, x1_high, x1_low
Send PASET (0x2B) command + 4 data bytes: y0_high, y0_low, y1_high, y1_low
Send RAMWR (0x2C) to begin pixel data stream
After this, any 16-bit color values sent via TFT_WriteData16 fill the window left-to-right, top-to-bottom automatically β no coordinate math needed per pixel.
ILI9341 Init sequence (TFT_Init): After reset, the init sequence sends power control, VCOM, frame rate, display function, and finally MADCTL 0x28 (landscape orientation, BGR color order). PIXFMT 0x55 sets RGB565 β 5 bits red, 6 bits green, 5 bits blue, 2 bytes per pixel.
What this file does: Decodes NEC protocol IR remote signals using a falling-edge interrupt on PB10 and TIM2 as a free-running 1 MHz stopwatch. This is entirely interrupt-driven β the main loop just polls g_ir_ready.
The NEC Protocol:
Leader pulse: 9ms HIGH + 4.5ms LOW
Bit '0': 562Β΅s HIGH + 562Β΅s LOW (total ~1.12ms)
Bit '1': 562Β΅s HIGH + 1687Β΅s LOW (total ~2.25ms)
Frame: 32 bits = address(8) + ~address(8) + command(8) + ~command(8)
Initialization (IR_Init):
- Enable GPIOB, SYSCFG, and TIM2 clocks.
- Set
PB10as input with pull-up (the IR receiver output is active-LOW). - Configure TIM2: PSC=15 β 1 Β΅s ticks, ARR=0xFFFFFFFF (free-running 32-bit counter), start it.
- Route
EXTI10to port B viaSYSCFG_EXTICR3. - Enable EXTI10 in
EXTI_IMR, configure falling-edge trigger inEXTI_FTSR, disable rising edge inEXTI_RTSR. - Enable IRQ 40 (
EXTI15_10) inNVIC_ISER1bit 8.
Interrupt handler (EXTI15_10_IRQHandler):
The handler runs a 3-state machine, executing in microseconds on each falling edge:
State 0 (IDLE):
β First falling edge seen. Save TIM2 count, move to State 1.
State 1 (ARMED β waiting for leader):
β Measure gap since last falling edge.
β If 12500β14500 Β΅s: valid 9ms+4.5ms leader detected β State 2, reset bit counter.
β Otherwise: stay in State 1 (wait for a fresh leader).
State 2 (RECEIVING BITS):
β Classify gap:
900β1400 Β΅s β bit = 0
1800β2800 Β΅s β bit = 1
Anything else β bad timing, reset to State 0.
β Shift bit into ir_temp_code (LSB first).
β After 32 bits: call IR_PublishIfValid.
Checksum validation (IR_PublishIfValid): Extracts the 4 bytes from the 32-bit code. Verifies addr + ~addr = 0xFF and cmd + ~cmd = 0xFF. If either check fails, the frame is silently dropped. Only the 8-bit command byte is published to g_ir_raw_code.
Tricky part: The falling-edge-only approach means the timing window is measured between falling edges, not pulse widths. This is more noise-resistant than measuring pulse HIGH time because the sensor output has slow rise times that distort pulse width measurements. The 900β1400 Β΅s and 1800β2800 Β΅s windows are intentionally wide to handle clock tolerance across different remote brands.
What these files do: Implement a full two-way Bluetooth communication layer over USART2, connecting the robot to the Robo Mobile App via an HC-05 module. The layer handles three responsibilities cleanly separated from the rest of the code: receiving and parsing motion commands from the app, periodically transmitting structured vitals packets to the app, and remotely dismissing alerts (smoke and medication) over the air.
Three-file split:
| File | Role |
|---|---|
bluetooth_constants.s |
All EQU definitions β USART2 register offsets, baud rate value, GPIO masks, buffer sizes, ASCII codes, command IDs |
bluetooth_buffer.s |
RAM allocations only β the RX/TX buffers and all shared flags that the motion layer reads |
bluetooth.s |
All executable logic β init, receive task, parse, transmit task, packet builders |
USART2 Initialization (BT_Init):
- Enable GPIOA clock and USART2 clock (
RCC_APB1ENRbit 17). - Set
PA2andPA3to alternate function mode (MODERbits 10), assignAF7(USART2) inGPIO_AFRL. - Apply pull-up only on
PA3(RX) βPA2(TX) is left floating. Without the pull-up, a disconnected RX line floats and generates framing errors that fill the RX buffer with garbage. - Set output speed to fast on both pins (needed to cleanly drive 9600 baud transitions).
- Configure USART2: clear CR1/CR2/CR3, write
BRR = 0x0683(derived from 16MHz / 9600 = 1666.7, mantissa 104, fraction 3 in OVER8=0 mode), then write CR1 = UE + TE + RE to enable the peripheral with both TX and RX active. - Clear any stale byte already sitting in DR by reading it if RXNE is set on startup.
- Zero all flags:
g_bt_cmd_ready,g_bt_motion_mode_request,g_bt_motion_dir_request, all tick timestamps.
Receiving a command (BT_RxTask):
Called once per main loop iteration. Deliberately reads at most one full line per call to avoid starving the scheduler.
- Poll USART2_SR for RXNE (Receive Not Empty):
- No byte ready: exit immediately (non-blocking).
- Byte ready: read USART2_DR (clears RXNE automatically).
- CR (0x0D): ignore, continue polling.
- LF (0x0A): line complete β null-terminate buffer β call
BT_ParseLineβ clear buffer. - Any other byte: append to
bt_rx_buffer[bt_rx_index++].- If
bt_rx_index >= 79(buffer full): discard entire line (overflow protection).
- If
- Every received byte also updates
g_bt_last_rx_tickwith the current SysTick millisecond count. The motion layer uses this timestamp to detect the 2-second BT inactivity timeout and resume autonomous line tracking.
Parsing a command (BT_ParseLine):
Uses substring matching rather than exact string equality. This means the app can send "CMD=FWD\n", "DIR:FWD\n", or just "FWD\n" β any format containing the keyword is accepted. This made the app development much easier and made the robot robust to minor protocol changes.
; BT_Contains scans bt_rx_buffer byte-by-byte using BT_StartsWith at each position
; BT_StartsWith does a byte-by-byte comparison until the substring runs out (match) or a mismatch
Check for "FWD" -> BT_SetDirRequest(BT_DIR_FWD=1) + BT_QueueACK
Check for "BACK" -> BT_SetDirRequest(BT_DIR_BACK=2) + BT_QueueACK
Check for "LEFT" -> BT_SetDirRequest(BT_DIR_LEFT=3) + BT_QueueACK
Check for "RIGHT" -> BT_SetDirRequest(BT_DIR_RIGHT=4) + BT_QueueACK
Check for "STOP" -> BT_SetDirRequest(BT_DIR_STOP=5) + BT_QueueACK
Check for "PHONE" -> BT_SetModeRequest(BT_MODE_PHONE=2)+ BT_QueueACK
Check for "LINE" -> BT_SetModeRequest(BT_MODE_LINE=1) + BT_QueueACK
Check for "CMD=UI" -> BT_Handle_UIKey + BT_QueueACK (virtual keypad)
Check for "OFF" -> check if also contains "MED" or "SMOKE" -> clear alarm flagBT_SetDirRequest and BT_SetModeRequest both write to the shared RAM flags (g_bt_motion_dir_request, g_bt_motion_mode_request, g_bt_cmd_ready = 1) that the motion layer polls each cycle. Only one of the two request fields is set per command β the other is explicitly zeroed to prevent stale commands.
Remote alarm dismissal (BT_Handle_SmokeAlertOff / BT_Handle_MedAlertOff):
When the app sends a command containing both "OFF" and "SMOKE", the handler clears Smoke_Alert_Flag from g_alarm_flags, resets g_smoke_ignore_counter to the ignore threshold (prevents immediate re-trigger), and if the current state is STATE_SMOKE_ALERT, forces g_sys_state back to STATE_MAIN_MENU. The medication equivalent does the same for Med_Alert_Flag. This means a nurse can dismiss robot alarms from their phone without physically touching the robot.
Transmitting packets (BT_TxTask + BT_PeriodicTask):
The TX side uses a flat byte buffer in RAM (bt_tx_buffer, 192 bytes). Packets are assembled in-place by calling:
BT_StartPacketβ resetsbt_tx_lenandbt_tx_indexto 0BT_AppendStringβ copies a null-terminated string from flash intobt_tx_bufferbyte by byteBT_AppendU32β converts a 32-bit integer to decimal ASCII digits usingUDIV/MLS(digits are built in reverse order intobt_num_buffer, then copied forward)BT_AppendCharβ appends a single character, always keeps the buffer null-terminated
BT_TxTask sends the assembled packet by polling USART_SR_TXE (Transmit Empty) before writing each byte to USART_DR. Critically, it calls BT_RxTask on every iteration of the TX wait loop β so incoming joystick commands are never dropped while a vitals packet is being sent out.
BT_PeriodicTask β the scheduler for outgoing packets:
Every main loop call:
- Run
BT_TxTask(drain any pending bytes). - If TX buffer is idle:
a.
BT_CheckMedEventβ ifsys_statejust leftSTATE_MED_DISPENSE, queue"TYPE=MED_EVENT,...,STATUS=DISPENSED\r\n". b.BT_CheckSmokeAlertβ ifSmoke_Alert_Flagis set and 5000ms since last alert TX, queue"TYPE=ALERT,...,SMOKE=<level>\r\n". c. Every 250ms:BT_QueueVitalsβ queue full vitals packet.
Vitals packet format (sent every 250ms):
TYPE=VITALS,PATIENT=001,BPM=<bpm>,SPO2=<spo2>,BREATH=<breath>,SMOKE=<smoke>,MED=<timer>,ALERT=<NONE|SMOKE_DETECTED|MED_ALERT|SMOKE_AND_MED>\r\n
All numeric values (g_bpm, g_spo2, g_breath_level, g_smoke_level, g_med_timer) are read directly from shared RAM and converted to decimal ASCII on the fly by BT_AppendU32. The ALERT field combines both alarm flags with bitwise AND checks to produce one of four string values.
Tricky part β BT_AppendU32 digit reversal: The standard decimal conversion (% 10 loop) naturally produces digits in LSB-first order (least significant digit first). The function writes them into bt_num_buffer (12 bytes) in that reversed order, tracking the count in R5, then copies them out in reverse to get the correct string. Special-casing 0 is also required because the loop would exit immediately and produce an empty string otherwise.
Tricky part β LTORG placement: Every large function in bluetooth.s ends with ALIGN + LTORG. Without this, the assembler's literal pool (used for LDR R0, =some_address loads) can exceed 4KB distance from the instruction, causing an A1284E assembler error. Since this file is large and uses many address literals, LTORG blocks are mandatory after every ~30β50 instructions.
What this file does: Drives the HC-SR04 sensor to measure distance to obstacles. Uses TIM5 as a Β΅s stopwatch to measure the echo pulse duration, then converts it to centimeters.
Initialization (HCSR04_Init):
- Enable GPIOA clock. Enable TIM5 clock via
RCC_APB1ENRbit 3. - Set
PA12as output (Trig),PA15as input (Echo). - Ensure PA12 starts LOW (idle state).
- Configure TIM5: PSC=15 β 1 Β΅s ticks, ARR=0xFFFFFFFF (32-bit free-running), start with
TIM_CR1 = 1.
Reading distance (HCSR04_Read):
1. Pulse PA12 HIGH for ~100 loop cycles (β10 Β΅s) then LOW.
2. Wait for PA15 (Echo) to go HIGH β with a 10,000-count timeout.
3. Reset TIM5_CNT to zero (start stopwatch).
4. Wait for PA15 to go LOW β with a 0x20000-count timeout.
5. Read TIM5_CNT = echo duration in microseconds.
6. Distance (cm) = duration / 58
(derived from: distance = (duration Γ speed_of_sound) / 2
= (duration Γ 0.0343 cm/Β΅s) / 2 β duration / 58)
Returns 999 on either timeout, which the motion module treats as "no obstacle detected" to avoid false stops.
Tricky part: The two separate timeouts (one for Echo going HIGH, one for Echo going LOW) are intentional. If only one timeout covered the whole measurement, a long echo from a far object would time out incorrectly. The counter is reset only after Echo goes HIGH, so only the actual echo duration is measured, not the sensor's internal processing delay.
What this file does: Implements a non-blocking, debounced bedside/charging station detection system using an active-low infrared receiver module connected to PB13. This allows the robot to align with and dock at bedside locations to deliver clinical care.
Initialization (StationIR_Init):
- Clear the local variables
station_debounce_cntandg_station_detectedin RAM to zero. - Enable the clock for GPIOB (where
STATION_IR_PORTisGPIOB_BASE) by callingGPIO_EnableClock. - Configure
PB13(STATION_IR_PIN) as a digital input by callingGPIO_ConfigInput.
Debounced Detection Logic (StationIR_Update):
Called frequently in the main scheduler loop (via Main_BackgroundTasks). It implements a 5-sample consecutive debounce filter to filter out ambient infrared noise:
; Read the pin and invert it since the IR sensor output is active-LOW (0 = active/detected)
LDR R0, =STATION_IR_PORT
MOVS R1, #STATION_IR_PIN
BL GPIO_ReadPin
EOR R0, R0, #1 ; Invert logic (0 -> 1 = detected, 1 -> 0 = not detected)- If the current pin state matches the globally stored status (
g_station_detected), the debounce counter (station_debounce_cnt) is reset to0. - If the pin state differs from the current global status, the debounce counter is incremented by
1. - When the debounce counter reaches
DEBOUNCE_THRESHOLD = 5, it toggles the global state variableg_station_detectedto the new stable reading and resets the counter to0.
Checking detection state (StationIR_IsDetected):
Returns the debounced state stored in g_station_detected (returns 1 if the robot is aligned at a station, 0 if not) in register R0.
Tricky part β Active-Low Signal & Ambient Noise Filtering: Since the sensor output is active-low (pulls to 0 when sensing an IR beacon), raw readings must be inverted with EOR R0, R0, #1. Without the 5-sample debounce verification window, ambient light fluctuations in a hospital corridor or sensor jitter would trigger false stops, causing the robot to dock prematurely.
Every source file in the repository corresponds to a modular component of the robot's hardware or logical flow. Below is the full file-by-file breakdown highlighting the logic and registers utilized.
-
core/main.s:- Purpose: The primary scheduler and initialization module.
-
Logic: Performs memory sweeps during boot (
Main_InitGlobals), calls setup subroutines, and sets up a$1\text{ms}$ metronome interrupt using the SysTick timer reload register. It schedules non-blocking checks in a loop, routes states, and throttles visual drawing refreshes.
-
core/constants.s:- Purpose: Hardware and state symbol definitions.
-
Logic: Defines assembly constants (
EQU) for peripheral addresses (RCC, GPIO, ADC, TIM, USART, SPI), TFT color profiles, state indices, and key codes. Contains no executable code to save memory.
-
core/global.s:- Purpose: Declares shared variables in RAM.
-
Logic: Declares 32-bit aligned variables (using the
SPACE 4command) within theVARIABLESSRAM read-write data section. These variables areEXPORTed for project-wide visibility.
-
core/gpio.s:- Purpose: Low-level port controller library.
-
Logic: Implements modular routines to enable peripheral clocks (
GPIO_EnableClock) viaRCC_AHB1ENR, configure inputs (GPIO_ConfigInput), configure outputs (GPIO_ConfigOutput), write pin states (GPIO_WritePin/GPIO_ClearPin) using the bit set/reset register (GPIO_BSRR), and read pin states (GPIO_ReadPin) using the input data register (GPIO_IDR).
-
core/motion_constants.s:- Purpose: Static motion parameters.
-
Logic: Contains
EQUconstants for motor speed PWM duty cycles (straight driving, slow turning, tank spin-in-place) and the 2-second Bluetooth manual control timeout fail-safe.
-
core/ui_state.s:- Purpose: Core UI event router and state dispatcher.
- Logic: Processes raw IR keypresses, coordinates menu navigation, handles global alarms (fire and medication alert overrides), clears the TFT screen during transitions, and limits wave plotting update rates.
-
features/breathing.s:- Purpose: Breathing waveform processor.
-
Logic: Samples
PA0via the ADC, tracks baseline drift to keep the signal centered, and amplifies the signal to display scrolling breathing waveforms on the TFT screen.
-
features/ir_stations.s:- Purpose: Bedside call and alignment module.
- Logic: Monitors bedside IR call beacons. When a call signal is detected, the robot stops to administer clinical care.
-
features/max.s:- Purpose: Vitals acquisition module using the MAX30102.
-
Logic: Manages hardware I2C read and write transactions. It configures I2C registers, checks FIFO pointers, reads raw red and infrared channel data, filters raw values, and calculates BPM and blood oxygen saturation (
$SpO_2$ ) when a finger is detected.
-
features/medicine.s:- Purpose: Medication scheduler and dispenser actuator.
-
Logic: Converts user input into a background countdown. Once the timer reaches zero, the system sounds an alarm, waits for user confirmation, and rotates a positional servo motor using step-by-step PWM pulses on
PA6($0^{\circ}$ at$500\text{ }\mu\text{s}$ ,$90^{\circ}$ at$1500\text{ }\mu\text{s}$ ,$180^{\circ}$ at$2500\text{ }\mu\text{s}$ ).
-
features/motion_bt.s:- Purpose: Bluetooth driving command parser.
- Logic: Maps Bluetooth remote commands (forward, backward, spin turn, stop) to low-level motor drivers.
-
features/santizing.s:- Purpose: Automatic gel dispenser module.
-
Logic: Reads the active-low proximity sensor on
PA4. When a hand is detected, the system activates the relay pump onPA5and runs a software delay loop to dispense gel.
-
features/smoke.s:- Purpose: Fire alarm monitor.
-
Logic: Samples the MQ-2 sensor on
PA1. It implements a 30-second startup delay to allow the sensor to warm up, averages readings to prevent false alarms, and flags a system-wide fire alarm if sustained smoke is detected.
-
features/stress.s:- Purpose: Heart rate volatility analyzer.
-
Logic: Calculates a psychological stress score based on heart rate fluctuations:
$$\text{Stress Score} = (\text{BPM} - 60) \times 2$$ It adds subtle visual noise (g_ms_ticks & 3) to keep the graph display animated.
-
features/ultrasonic.s:- Purpose: Collision avoidance module.
-
Logic: Emits trigger pulses on trigger pin
PC15and reads the return pulse duration on echo pinPC14to calculate the distance to obstacles for safety stops.
-
features/vein.s:- Purpose: Sub-dermal vein finder.
-
Logic: Samples the IR reflectance sensor on
PA7. It averages 8 readings to filter out noise, establishes a baseline from 128 readings, and maps light absorption levels to dynamic buzzer beep frequencies to guide clinicians.
-
Drivers/adc.s:- Purpose: Bare-metal ADC1 driver.
-
Logic: Enables the ADC1 peripheral clock, configures
PA0andPA1pins for analog mode, and sets sample time sequences. It also configures the Analog Watchdog (AWD) to monitor channel 1 and trigger interrupts in the NVIC if readings exceed thresholds.
-
Drivers/i2c.s:- Purpose: Hardware I2C1 Master driver.
-
Logic: Implements standard I2C start, stop, write, read, ack, and nack operations by managing the I2C1 hardware peripheral over open-drain SDA (
PB9) and SCL (PB8) lines.
-
Drivers/pwm.s:- Purpose: Hardware Timer PWM driver.
-
Logic: Sets up
TIM3andTIM4registers to output PWM waveforms for the positional medicine servo and motor speed controllers.
-
Drivers/bluetooth.s&Drivers/bluetooth_buffer.s:- Purpose: USART2 driver and ring buffers.
-
Logic: Configures
PA2(TX) andPA3(RX) to alternate functions. It implements non-blocking serial communication using USART interrupts and circular ring buffers in RAM.
-
Drivers/buzzer.s:-
Purpose: Beeper driver on
PB4. - Logic: Manages periodic buzzer beeps during active alarm states.
-
Purpose: Beeper driver on
-
Drivers/tft_low.s:- Purpose: ILI9341 register and SPI1 driver.
-
Logic: Configures
PB0(CS),PB1(DC),PB2(RST), and SPI1 alternate function pins (PB3/PB5). It configures the SPI1 register block, manages command and data modes, and initializes the ILI9341 3.2" display.
-
Drivers/tft_gfx.s:- Purpose: Custom 2D graphics rendering engine.
- Logic: Defines subroutines to clear the screen, define active pixel coordinate windows, fill rectangular blocks, render ASCII characters using custom font arrays, plot coordinate vectors, and draw real-time scrolling wave graphs.
-
motion.s(Root File):- Purpose: Unified motor guidance manager.
-
Logic: Reads the line tracking sensor array, determines direction adjustments using a decision tree, and controls motor inputs. It also implements safety checks that stop the robot if an obstacle is detected within
$15\text{cm}$ or when bedside call beacons are aligned.
Below is the implementation matrix for the robot's active features, detailing both high-level metaphors and bare-metal file collaborations.
Metaphor (ELI5): When your heart beats, blood rushes through your finger and absorbs light. The oximeter sensor shines red and infrared lights into your skin to count how fast your pulse is going and see how clean your blood oxygen is!
Detailed Implementation:
-
I2C Layer (
Drivers/i2c.s): Handles hardware register communication over open-drain pinsPB8(SCL) andPB9(SDA) with a standard 100 kHz transmission clock. It manages I2C transaction protocols: generating start, write-address (0x57 device LSB=0), sub-register write, read-address (LSB=1), repeated start, ACK, NACK, and STOP conditions. -
Configuration & Filtering (
features/max.s):- Initializes the MAX30102 by writing
MODE_RESET = 0x40to register0x09(REG_MODE_CFG), disabling interrupts, clearing FIFO pointers, configuring SpOβ mode, and setting both LED currents to1Fh(approx. 6.4mA). - Polls the FIFO pointers (
REG_FIFO_WR_PTR = 0x04andREG_FIFO_RD_PTR = 0x06) to check for new samples. If a sample exists, it executes a 6-byte burst read ofREG_FIFO_DATA = 0x07to retrieve raw Red and Infrared channel readings. - Implements an IIR DC Removal Filter to isolate the AC heart signal for screen plotting:
$$DC(n) = DC(n-1) + \frac{Raw(n) - DC(n-1)}{16}$$ $$AC(n) = Raw(n) - DC(n)$$ The AC signal is offset by +2000 to maintain positive values and stored ing_hr_ac_valfor real-time scrolling wave drawing on the TFT. - Implements finger detection: if the raw IR value falls below 40,000 counts, it flags "No Finger" and zeroes the outputs.
- Calculates BPM and SpOβ: BPM is mapped to a diagnostic range of 70 to 101 bpm based on the Red raw values. SpOβ is calculated by taking the ratio of Red and Infrared AC/DC components:
$$\text{Ratio} = \frac{Red_{AC} / Red_{DC}}{IR_{AC} / IR_{DC}}$$ This ratio is scaled and clamped to output a value between 70% and 100% tog_spo2in RAM.
- Initializes the MAX30102 by writing
-
Scheduler (
core/main.s): InvokesHR_ReadFIFOinside the super-loop execution cycle. -
Visual Interface (
core/ui_state.s&Drivers/tft_gfx.s): Renders numeric BPM and SpOβ digits, and draws real-time scrolling raw PPG waves on the TFT.
Metaphor (ELI5): Think of this as drawing a wave line on a chalkboard that tracks your breathing. To prevent the line from drifting off the board, a helper calculates the average (baseline) and automatically centers the drawing line.
Detailed Implementation:
-
ADC Driver (
Drivers/adc.s): Samples the analog respiratory flow sensor connected toPA0(ADC Channel 0) at a resolution of 12 bits (0 to 4095 range). -
Signal Processing (
features/breathing.s):- Restricts update tasks to a stable 40 Hz (25ms refresh cycle) by comparing uptime ticks in
g_ms_ticks. - On startup, initializes baseline and filter registers with the first raw sensor sample.
- Implements a slow baseline tracking filter to eliminate drift:
$$Baseline(n) = Baseline(n-1) + \frac{Raw(n) - Baseline(n-1)}{64}$$ - Calculates the centered AC breathing level by subtracting the baseline and amplifying by 8:
$$AC_{unfiltered} = (Raw(n) - Baseline(n)) \times 8$$ This value is clamped within$[-1024, +1024]$ to prevent overflow. - Smoothes out high-frequency noise using a low-pass filter:
$$Filtered(n) = Filtered(n-1) + \frac{AC_{unfiltered}(n) - Filtered(n-1)}{4}$$ - Shifts the smoothed AC signal to center around a baseline offset of 2048 and stores the final result in
g_breath_levelin RAM.
- Restricts update tasks to a stable 40 Hz (25ms refresh cycle) by comparing uptime ticks in
-
Scheduler (
core/main.s): ExecutesBREATHE_Updaterepeatedly in the scheduler. -
Visual Interface (
core/ui_state.s&Drivers/tft_gfx.s): Draws the scrolling waveform ofg_breath_levelrelative to the screen height.
Metaphor (ELI5): When you are calm, your heart beats steadily. When you are excited or stressed, your heart rate increases. This script calculates how high your heart rate is compared to resting, and adds micro-movements on the screen to show live feedback.
Detailed Implementation:
-
Data Source (
features/max.s): Obtains live heart rate values fromg_bpm. -
Stress Analysis (
features/stress.s):- Calculates the stress index using the mathematical relationship:
$$\text{Stress Score} = (BPM - 60) \times 2$$ - If the calculated score is negative (BPM is below 60), it defaults to 0.
- Limits the maximum stress score to 100 to prevent chart overflows.
- Calculates the stress index using the mathematical relationship:
-
Jitter Injection (
core/ui_state.s): Reads the millisecond counterg_ms_ticksto inject a 0β3% variation mask (g_ms_ticks & 3) to the displayed volatility score, simulating real-life sensor jitter and keeping the TFT interface visually dynamic. -
Visual Interface (
Drivers/tft_gfx.s): Renders the stress level as an animated bar gauge alongside warning tags.
Metaphor (ELI5): Veins look like dark underground rivers under our skin. The robot shines an invisible flashlight (infrared) down. If it finds a river (vein), it absorbs the light, making the robot beep faster the closer it gets to the center.
Detailed Implementation:
-
ADC Driver (
Drivers/adc.s): ConfiguresPA7(ADC Channel 7) for analog sensing. Writes to theADC_SMPR2register to apply an extended 480 clock cycle sampling time, stabilizing reading acquisitions and filtering out high-impedance optical noise. -
Signal Processing (
features/vein.s):- Performs an 8-sample moving average filter on the raw ADC value to eliminate high-frequency ripple.
- On startup, records a baseline value over the first 128 samples to calibrate individual skin reflectance.
- Calculates the relative sub-dermal absorption:
$$\text{Absorption} = \text{Baseline} - \text{Averaged Readings}$$
-
Audio Modulation (
Drivers/buzzer.s): Maps the absorption level to a dynamic beep interval. When absorption is high (indicative of a sub-dermal blood vessel absorbing IR light), the beep interval decreases down to a solid tone to guide needle insertion. -
Visual Interface (
core/ui_state.s&Drivers/tft_gfx.s): Graphs the absorption intensity as a live horizontal scrolling wave.
Metaphor (ELI5): This is a digital thermometer that reads your body temperature and splits it into a whole number (like 37) and a fraction (like .5) to show on the screen.
Detailed Implementation:
- I2C Layer (
Drivers/i2c.s): Performs reads and writes on the MAX30102 temperature registers. - Acquisition (
features/max.s):- Initiates a temperature conversion by writing
0x01toREG_TEMP_CONFIG = 0x21. - Spins in a watchdog-guarded timeout loop (40,000 counts) checking for the conversion bit to clear.
- Reads the signed integer byte from
REG_TEMP_INTR = 0x1Fand stores it tog_temp_int. - Reads the fractional part (4-bit resolution, each step representing 0.0625Β°C) from
REG_TEMP_FRAC = 0x20, masking with0x0Fand storing the decimal coefficient tog_temp_frac.
- Initiates a temperature conversion by writing
- Scheduler (
core/main.s): TriggersHR_ReadTempconversions every 500ms. - Visual Interface (
core/ui_state.s&Drivers/tft_gfx.s): Renders decimal readouts on the display.
Metaphor (ELI5): The robot acts like a smart pillbox. You set a timer, and when it runs out, the robot rings a bell (buzzer) and turns a wheel (servo) to drop a pill into your hand!
Detailed Implementation:
- State Machine (
core/ui_state.s): Captures keypad inputs to decrement or increment medication wait times, stored ing_med_timer. - Background Timer (
features/medicine.s): Compares current SysTick values againstg_ms_ticksto handle background seconds countdown. Wheng_med_timerreaches 0, it triggers a global alarm flagMed_Alert_Flaging_alarm_flags. - PWM Driver (
Drivers/pwm.s): Configures TIM3 onPA6for 50Hz PWM output (prescaler 15, ARR 19999). Adjusts duty cycle pulse widths viaTIM3_CCR1:- 500 Β΅s (0.5ms / 2.5% duty) β 0Β° (holding position)
- 1500 Β΅s (1.5ms / 7.5% duty) β 90Β° (medication dropped)
- 2500 Β΅s (2.5ms / 12.5% duty) β 180Β° (sweep complete)
- Buzzer & Alarm Control (
Drivers/buzzer.s&core/main.s): Sounds alerts and waits for user confirmation inputs before returning the servo to the holding position.
Metaphor (ELI5): If the robot smells smoke, it counts to 15 to make sure it's not just a false alarm (like a blown candle), then sounds a fire alarm and switches the screen to a warning display.
Detailed Implementation:
- ADC Driver (
Drivers/adc.s): ConfiguresPA1(ADC Channel 1) for the MQ-2 sensor. Configures the hardware Analog Watchdog (AWD) to trigger the high-priorityADC_IRQHandler(IRQ 18) when readings exceed the upper threshold (HTR = 3000). - Sensor Warmup & Verification (
features/smoke.s):- Implements a 30-second startup delay (150 readings) to allow the MQ-2 heating element to stabilize before checking alarm flags.
- Implements a debounce filter requiring 15 consecutive samples above threshold over 3 seconds to prevent false alarms.
- Audio Modulation (
Drivers/buzzer.s): Drives continuous alert beeps onPB4until smoke concentration drops below 2000. - Visual Interface (
core/ui_state.s): SetsSmoke_Alert_Flaging_alarm_flags, overriding the active screen to render a red flashing "SMOKE ALERT - DANGER" warning.
Metaphor (ELI5): When you place your hand under the sensor, it blocks a light beam. The robot detects this and turns on a pump to dispense sanitizer, then turns it off.
Detailed Implementation:
- State Machine (
features/santizing.s):- Polls the active-low proximity sensor connected to
PA4usingGPIO_ReadPin. - If a hand is detected (
PA4 == 0), it pulls the relay pump outputPA5HIGH to start the gel pump. - Runs a software delay loop to keep the pump active for exactly 1.5 seconds.
- Pulls
PA5LOW to stop the pump, preventing gel leakage. - Enforces a mandatory 2-second cooldown delay before another dose can be dispensed.
- Polls the active-low proximity sensor connected to
Metaphor (ELI5): The robot displays a circle with a small gap on the screen (like a letter "C"). You press arrow buttons on the remote to point to where the gap is. If you get it right, the circle gets smaller and smaller!
Detailed Implementation:
- IR Receiver (
Drivers/ir_driver.s): Decodes directional remote keys (KEY_UP/KEY_DOWN/KEY_LEFT/KEY_RIGHT) via falling-edge interrupts onPB10. - Test Management (
core/ui_state.s):- Generates a randomized gap orientation (Up, Down, Left, or Right) using the lowest bits of
g_ms_ticks. - Decreases the size of the Landolt C ring by reducing the drawing radius on successive correct answers.
- Tracks test scores and calculates the corresponding Snellen visual acuity fraction (ranging from < 6/60 up to 6/6).
- Generates a randomized gap orientation (Up, Down, Left, or Right) using the lowest bits of
- Graphics Engine (
Drivers/tft_gfx.s): Renders the high-contrast Landolt C optotypes on the LCD.
Metaphor (ELI5): The robot acts like a toy train on a track. It uses three light sensors underneath to watch the floor. If it drifts too far left or right, it adjusts its wheels to stay on the line. If it loses the line entirely, it remembers where it last saw it and turns back!
The line tracking array consists of 3 IR sensors mounted underneath the chassis pointing to the floor, connected as inputs to Port B:
- Left Sensor:
PB12(LINE_LEFT) - Center Sensor:
PB15(LINE_CENTER) - Right Sensor:
PB14(LINE_RIGHT)
The main guidance loop reads these pins individually using GPIO_ReadPin, shifts them into position, and combines them into a single 3-bit status mask:
; Read and shift Left (bit 2), Center (bit 1), and Right (bit 0)
LDR R0, =GPIOB_BASE
MOV R1, #LINE_LEFT
BL GPIO_ReadPin
LSL R4, R0, #2 ; Left shifted to bit 2
LDR R0, =GPIOB_BASE
MOV R1, #LINE_CENTER
BL GPIO_ReadPin
LSL R5, R0, #1 ; Center shifted to bit 1
LDR R0, =GPIOB_BASE
MOV R1, #LINE_RIGHT
BL GPIO_ReadPin
MOV R6, R0 ; Right stays at bit 0
ORR R7, R4, R5
ORR R7, R7, R6
EOR R7, R7, #7 ; Invert 3-bit mask (active-low correction)The Active-Low Inversion Battle: Most reflective sensors pull their digital outputs LOW when they detect a black line (absorbing infrared) and HIGH when on the white floor (reflecting infrared). To make the decision tree readable and intuitive, the code uses
EOR R7, R7, #7to invert the 3-bit mask. After inversion:1= Sensor is active over the black line,0= Sensor is active over the white floor.
The inverted 3-bit mask in R7 represents the position of the black line relative to the robot's center. The controller evaluates this state using a static branching tree:
Inverted Mask (R7) |
Binary | Sensors Over Line | Target Movement | Action Taken |
|---|---|---|---|---|
0x02 |
010 |
Center | Drive Straight | Forward, Left=340, Right=340 |
0x05 |
101 |
Left & Right | Drive Straight (Junction) | Forward, Left=340, Right=340 |
0x04 |
100 |
Left | Steer Left (Arc) | Forward, Left=480, Right=280 |
0x06 |
110 |
Left & Center | Steer Left (Arc) | Forward, Left=480, Right=280 |
0x01 |
001 |
Right | Steer Right (Arc) | Forward, Left=290, Right=480 |
0x03 |
011 |
Right & Center | Steer Right (Arc) | Forward, Left=290, Right=480 |
0x07 |
111 |
All Three | T-Junction / Stop | Halt motors (Left=0, Right=0) |
0x00 |
000 |
None | Line Lost (Rescue) | Jump to Memory Rescue |
When the robot moves too fast or encounters a sharp 000 (all sensors over white floor). Without state memory, the robot would continue driving straight into the wall or spin randomly.
To solve this, the driver uses a global variable Last_Turn saved in RAM:
0= Last moving straight.1= Last steered left.2= Last steered right.
Whenever the line is lost (R7 == 0x00), the Rescue_Lost_Line subroutine triggers:
- If
Last_Turn == 1(line lost while steering left): The robot initiates a high-torque, counter-rotational Pivot Spin Left (Action_Pivot_Left). - If
Last_Turn == 2(line lost while steering right): The robot initiates a high-torque, counter-rotational Pivot Spin Right (Action_Pivot_Right). - This forces the robot to swing back onto the line in the exact direction it was lost, recovering automatically.
The robot uses two L298N-type motor drivers. Direction is controlled by GPIO outputs on Port A, while velocity is controlled by TIM4 PWM outputs on Port B.
H-Bridge Direction Vectors (PA8 to PA11):
- Left Motor Control:
MOT_IN1(PA8),MOT_IN2(PA9) - Right Motor Control:
MOT_IN3(PA10),MOT_IN4(PA11)
; Forward Vector
IN1=1, IN2=0, IN3=1, IN4=0 ; Both motors drive forward
; Backward Vector
IN1=0, IN2=1, IN3=0, IN4=1 ; Both motors drive backward
; Tank Spin Left Vector
IN1=1, IN2=0, IN3=0, IN4=1 ; Left motor forward, Right motor backward
; Tank Spin Right Vector
IN1=0, IN2=1, IN3=1, IN4=0 ; Left motor backward, Right motor forwardPWM Velocity Control (TIM4 CH1 & CH2 on PB6/PB7):
The timer auto-reload register (ARR) is set to 999 to produce a TIM4_CCR1 and TIM4_CCR2):
- Straight: Left =
340, Right =340(balanced forward cruising) - Arc Left: Left =
480, Right =280(asymmetrical drift to correct rightward error) - Arc Right: Left =
290, Right =480(asymmetrical drift to correct leftward error) - Pivot Turn: Left =
280, Right =280(medium-speed opposite spin to search for the line)
We spent countless debugging sessions solving two critical hardware issues in motion.s:
1. The 8-Byte Stack Alignment Hard Faults:
The ARM Cortex-M4 ABI enforces 8-byte stack alignment at public function boundaries. Because helper functions like Motion_Forward call sub-routines (e.g. GPIO_WritePin and PWM_Set_Motor_Speed), pushing only a single register (PUSH {LR}) shifts the stack pointer (SP) by 4 bytes (odd-word alignment), resulting in immediate Hard Faults when nesting calls. The fix was to push a dummy register R3 to align the stack:
Motion_Forward
PUSH {R3, LR} ; R3 is a dummy push to align SP to 8 bytes!
...
POP {R3, PC}2. Arc Turn vs. Pivot Turn Speed Balancing:
During initial testing in simulation, if the arc speeds were too high, the robot would swing violently from side to side (oscillating out of control). If the speeds were too low, it stalled on turns. Similarly, pivot recovery spins required opposite wheel rotation at moderate speeds (280 out of 999) to prevent the robot from spinning past the line before the sensor polling task could capture the transition. The values (480/280 for arcs, 280/280 for pivots) were manually calibrated through repeated iteration.
File Collaboration:
-
core/gpio.s: ConfiguresPB12,PB15, andPB14as digital inputs. -
motion.s: Reads tracking inputs, processesLast_Turnmemory states, and stops if the ultrasonic sensor registers an obstacle within$15\text{ cm}$ . -
Drivers/pwm.s: DirectsTIM4registers to output correct PWM speeds to the DC motors.
Metaphor (ELI5): Usually, the robot drives itself. But if you connect your phone over Bluetooth, you can drive it like a remote-controlled car! If you close the app, the robot safely stops and goes back to tracking the line.
Detailed Implementation:
- Serial Buffer (
Drivers/bluetooth.s&Drivers/bluetooth_buffer.s): Sets up USART2 onPA2/PA3to receive joystick inputs. Fills a circular ring buffer in the background using RXNE interrupts. - Command Parsing (
features/motion_bt.s): Searches the buffer for motion commands (FWD, BACK, LEFT, RIGHT, STOP). Sets manual override flags and resets a 2-second timeout watchdog on each new packet. - Override Logic (
motion.s): If the manual flag is set, it bypasses autonomous line tracking to execute Bluetooth steering commands. If the 2-second timeout expires, it halts the robot and returns to autonomous line-tracking mode.
Metaphor (ELI5): Imagine the robot is a mail carrier. When a patient clicks a calling button at their bedside, it turns on an invisible beacon (Infrared light). The robot is drawn to this light, navigates to the bedside, and stops to deliver medication.
File Collaboration:
core/constants.s&core/gpio.s: Configures pinPB13(STATION_IR_PIN) as a digital input.features/ir_stations.s: Monitors sensor inputs onPB13. Inverts the active-low signal and runs a 5-cycle debounce filter to setg_station_detected.core/main.s: CallsStationIR_Updateon every super-loop cycle.motion.s: Checks the status ofg_station_detected. If active (alignment beacon detected), the guidance loop callsMOT_StopNowto halt the DC motors, docking the robot at the patient's bedside.
The challenge: a hospital ward may have multiple bedsides, but the robot must always dock at exactly one at a time. Rather than adding another microcontroller to arbitrate, a fully passive hardware circuit handles this using three inexpensive components.
If two patients press their call buttons simultaneously, two IR beacons would emit at once. The robot's single IR receiver on PB13 cannot tell them apart β it would detect a combined signal and navigate ambiguously between the two stations. The circuit ensures this situation is physically impossible: only one IR transmitter can be powered at any moment, regardless of how many buttons are pressed.
| Component | Part | Role |
|---|---|---|
| Latch push-buttons | Momentary latching switches | Patient calls the robot by pressing and holding their bedside button |
| 3-to-8 decoder | 74HC238 | Converts the 3-bit binary station address into a single active output line |
| Isolation diodes | Signal diodes (1N4148) | Prevent reverse current and cross-triggering between stations |
| IR LED transmitters | 940nm IR LEDs | Emit the infrared beacon that the robot detects on PB13 |
Step 1 β Patient presses a latch button.
Each bedside has a latching push-button. When pressed, it stores the call request mechanically (stays pressed until reset). The button outputs connect to the A, B, C select lines of the 74HC238 decoder, encoding which station is calling as a 3-bit binary number (Station 1 = 001, Station 2 = 010, Station 3 = 100, etc.).
Step 2 β The 74HC238 decoder activates exactly one output. The decoder reads the 3-bit address on its select lines and pulls exactly one of its eight outputs (Y0βY7) HIGH. All other outputs remain LOW. This is the core hardware guarantee β the decoder's combinational logic makes it physically impossible for two outputs to be HIGH simultaneously for the same select combination. Additionally, when more than 1 button is pushed, a corresponding LED right beside the nurse will light up simultaneously, instantly alerting her that the robot is needed at that exact time.
Step 3 β Diode isolation routes power to one IR transmitter.
Each decoder output (Y0, Y1, Y2, Y4) connects to one IR LED transmitter (the rest β Y3, Y5, Y6, Y7 β connect to regular LEDs) through a series diode. The diodes serve two purposes:
- Forward direction: current flows from the active decoder output through the diode to power the IR LED.
- Reverse direction: the diode blocks any back-current from flowing from one output into another, preventing cross-triggering where an active LED on Y1 could inadvertently feed current back into Y2's circuit.
Step 4 β The active IR beacon emits. Only the one IR LED connected to the active decoder output has a complete current path and emits infrared light at 940nm. All other IR LEDs remain off.
Step 5 β The robot detects and docks.
The robot's PB13 IR receiver module sees the single active beacon. StationIR_Update debounces the signal over 5 samples and sets g_station_detected = 1. The motion.s guidance loop calls MOT_StopNow, halting the robot precisely at that bedside station.
No microcontroller needed. The entire arbitration is handled combinationally by the 74HC238. This removes firmware complexity, eliminates interrupt latency, and reduces BOM cost significantly.
Deterministic priority. If multiple buttons are pressed simultaneously, the 74HC238 selects based on the binary value present on its select lines (whichever button combination produces the lowest or highest address wins). This gives predictable, repeatable behavior in a multi-patient scenario.
Diodes as one-way valves. The diode network is the safety layer. Without diodes, an active HIGH on Y1 could flow backward through the IR LED of station 2 and into the Y2 output pin of the decoder β potentially damaging the IC or falsely illuminating two LEDs. The diodes make each output's power path strictly one-directional.
Hospital-safe single-source guarantee. The robot always has exactly one docking target. This prevents ambiguous navigation in multi-room environments and ensures medication is delivered to the correct patient.
| Station | Button Address (C B A) | Active Decoder Output | IR Beacon |
|---|---|---|---|
| Station 1 | 0 0 1 |
Y1 | LED S1 ON |
| Station 2 | 0 1 0 |
Y2 | LED S2 ON |
| Station 3 | 0 1 1 |
Y3 | LED DUPLICATE ON |
| Station 4 | 1 0 0 |
Y4 | LED S3 ON |
| Station 5 | 1 0 1 |
Y5 | LED DUPLICATE ON |
| Station 6 | 1 1 0 |
Y6 | LED DUPLICATE ON |
| Station 7 | 1 1 1 |
Y7 | LED DUPLICATE ON |
| Stand by | 0 0 0 |
Y0 | LED STANDBY ON |
Figure: Proteus simulation of the low-cost IR bedside selection circuit. Latch buttons feed the
A/B/Cselect lines of the 74HC238 decoder. The single active output powers one IR LED through a diode, while all other LEDs remain off. The robot's receiver onPB13picks up the isolated beacon signal.
The robot is controlled via the custom RoboCare Mobile App β a full-featured Android application for patient monitoring, robot motion control, and remote alert management over Bluetooth (HC-05 / USART2).
βΆ Download RoboCare APK (Google Drive)
| Home Screen | Patient List | Vitals Dashboard | Motion Control |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Home Screen β Connect to HC-05 via Bluetooth, navigate to Patients, Bluetooth settings, or Motion Control.
Patient List β Add, edit, and delete patient profiles (name, ID, room number). Tap a patient to begin live monitoring.
Vitals Dashboard β Live BPM, SpOβ, Breathing level, Smoke level, Medication timer, and Alert status updated every 250ms from the robot's USART2 vitals stream. Save readings to patient history.
Motion Control β Full D-Pad joystick to drive the robot manually. Supports Forward, Backward, Left, Right, Stop, and toggle between Phone Control and autonomous Line Tracker mode.
| Feature | Description |
|---|---|
| Bluetooth Connection | Pair and connect to HC-05 module directly from the app |
| Patient Management | Add, edit, and delete patient profiles (Name, ID, Room) |
| Live Vitals Dashboard | Real-time BPM, SpOβ, Breath, Smoke level, Med timer, and Alert status |
| Save Readings | Persist vitals snapshots per patient with history log |
| Motion Control | D-Pad joystick to drive the robot (Forward, Back, Left, Right, Stop) |
| Phone / Line Mode | Switch robot between manual phone control and autonomous line tracking |
| Remote Alert Dismiss | Dismiss smoke and medication alerts over Bluetooth from the app |
| Virtual Keypad | Send UI navigation keys (0β9, AβD, CAM_UP/DOWN/LEFT/RIGHT) to control all robot screens remotely |
The app communicates with the robot over USART2 at 9600 baud via the HC-05 module. Commands are plain ASCII strings terminated with \r\n.
Motion Commands (parsed in motion_bt.s / bluetooth.s):
| Command String | Action |
|---|---|
DIR=FWD |
Move Forward |
DIR=BACK |
Move Backward |
DIR=LEFT |
Spin Left |
DIR=RIGHT |
Spin Right |
DIR=STOP |
Stop Motors |
MODE=PHONE |
Enter manual phone-control mode |
MODE=LINE |
Resume autonomous line tracking |
OFF,SMOKE |
Dismiss active smoke alert |
OFF,MED |
Dismiss active medication alert |
Virtual Keypad Commands (parsed via BT_Handle_UIKey in bluetooth.s):
| Command String | Key Injected | Effect |
|---|---|---|
CMD=UI,KEY=0 β¦ KEY=9 |
KEY_0 β¦ KEY_9 |
Navigate menus / select features |
CMD=UI,KEY=A β¦ KEY=D |
KEY_A β¦ KEY_D |
Function keys |
CMD=UI,KEY=CAM_UP |
KEY_UP |
D-Pad Up (Vision/Vein/Stress modes) |
CMD=UI,KEY=CAM_DOWN |
KEY_DOWN |
D-Pad Down |
CMD=UI,KEY=CAM_LEFT |
KEY_LEFT |
D-Pad Left |
CMD=UI,KEY=CAM_RIGHT |
KEY_RIGHT |
D-Pad Right |
The CAM_ prefix on directional keys ensures they do not collide with the robot's DIR=LEFT/RIGHT motion commands. The parser checks for CMD=UI first, routes through BT_Handle_UIKey, and stores the result in g_bt_ui_key_request. The main loop's Main_ProcessBTKeyInjection then writes this into g_keycode β making a virtual keypad press indistinguishable from a physical IR remote press.
Vitals Packet (transmitted by the robot every 250ms):
TYPE=VITALS,PATIENT=001,BPM=72,SPO2=98,BREATH=1840,SMOKE=120,MED=300,ALERT=NONE\r\n
The virtual keypad required coordinated changes across three assembly files to safely inject app D-Pad inputs into the robot's UI state machine without disturbing the motion command pipeline.
; Add to EXPORT list:
EXPORT g_bt_ui_key_request
; Add below g_bt_last_rx_tick:
g_bt_ui_key_request DCD 0 ; KEY_x value from CMD=UI,KEY=... (0=none)New strings in BT_RODATA (CAM_ prefix prevents collision with DIR=LEFT/RIGHT):
BT_TEXT_CMD_UI DCB "CMD=UI",0
BT_TEXT_KEY_PREFIX DCB "KEY=",0
BT_TEXT_KEY_UP DCB "KEY=CAM_UP",0
BT_TEXT_KEY_DOWN DCB "KEY=CAM_DOWN",0
BT_TEXT_KEY_LEFT DCB "KEY=CAM_LEFT",0
BT_TEXT_KEY_RIGHT DCB "KEY=CAM_RIGHT",0
BT_TEXT_KEY_0 DCB "KEY=0",0
; ... KEY=1 through KEY=9, KEY=A through KEY=DBT_ParseLine β New check (inserted after MODE=LINE, before OFF):
; 8. Check for CMD=UI,KEY=... (virtual keypad injection)
LDR R0, =bt_rx_buffer
LDR R1, =BT_TEXT_CMD_UI
BL BT_Contains
CMP R0, #1
BEQ BTP_UIKey
BTP_UIKey
BL BT_Handle_UIKey
BL BT_QueueACK
B BTP_ExitBT_Handle_UIKey subroutine (place at the bottom of bluetooth.s, above END):
Checks multi-char keys first (CAM_UP/DOWN/LEFT/RIGHT), then single-char keys (0β9, AβD). Uses BEQ.W (forced 32-bit branch encoding) to prevent assembler out-of-range errors. Stores the matched KEY_x constant into g_bt_ui_key_request.
BT_Handle_UIKey
PUSH {R4-R7, LR}
; Check CAM_UP first (multi-char, must precede single-char "U" check)
LDR R0, =bt_rx_buffer
LDR R1, =BT_TEXT_KEY_UP
BL BT_Contains
CMP R0, #1
BEQ.W BTUI_Up
; ... repeat for DOWN, LEFT, RIGHT, then 0-9, A-D
BTUI_Up
MOVS R4, #KEY_UP
B BTUI_Store
; ... other labels
BTUI_Store
LDR R5, =g_bt_ui_key_request
STR R4, [R5]
BTUI_Exit
POP {R4-R7, PC}
ALIGN
LTORG; Import the shared variable:
IMPORT g_bt_ui_key_request
; Updated Main_Loop (add after BT_RxTask):
Main_Loop
BL BT_RxTask ; Pull Bluetooth bytes
BL Main_ProcessBTKeyInjection ; β Inject virtual keypad presses
BL Main_ProcessBluetoothCmd ; Act on motion commands
BL Main_CheckIRInput ; IR remote
; ... rest unchanged
; Injection subroutine (place below Main_BackgroundTasks):
Main_ProcessBTKeyInjection
PUSH {R4-R6, LR}
LDR R4, =g_bt_ui_key_request
LDR R5, [R4]
CMP R5, #0
BEQ MPBK_Exit ; No pending key β skip
LDR R6, =g_keycode
STR R5, [R6] ; Inject into g_keycode (same as IR remote)
MOVS R5, #0
STR R5, [R4] ; Clear the request
MPBK_Exit
POP {R4-R6, PC}Design note: Writing into
g_keycodemeans the virtual keypad press flows through exactly the sameUI_Handle_Inputdispatch path as a physical IR remote press β no duplicate state-machine logic required anywhere.
- STMicroelectronics β STM32F401 Reference Manual
- Ilitek β ILI9341 Controller Datasheet
- Our Classmates and Partners β This repository serves as a shared base for low-level microprocessor collaboration.
Built with β€οΈ and ARM Assembly β no HAL libraries, no shortcuts, just registers.






