STM32实战:基于STM32F103的智能药盒定时提醒系统

文章目录

一、前言

1.1 技术背景

随着人口老龄化加剧和慢性病患者的增多,按时服药成为许多人日常生活中的重要环节。然而,由于记忆力减退、工作繁忙等原因,漏服、错服药物的情况时有发生,严重影响治疗效果。智能药盒作为一种辅助用药管理的设备,通过定时提醒、语音播报等功能,有效解决了这一问题。

STM32F103系列微控制器是意法半导体(STMicroelectronics)推出的基于ARM Cortex-M3内核的32位微控制器,具有高性能、低功耗、丰富的外设接口等特点,广泛应用于工业控制、消费电子、医疗设备等领域。其72MHz的主频、64KB512KB的Flash存储器和20KB64KB的SRAM,足以支撑智能药盒这类中等复杂度的嵌入式应用。

1.2 应用场景

智能药盒定时提醒系统适用于以下场景:

  • 老年人群体:记忆力减退,需要定时提醒服药
  • 慢性病患者:需要长期规律服药,如高血压、糖尿病等
  • 忙碌上班族:工作繁忙容易忘记服药时间
  • 儿童用药管理:家长需要提醒孩子按时服药
  • 医院/养老院:集中管理多位患者的用药情况

1.3 本文目标

通过本教程,你将学到:

  • STM32F103的RTC实时时钟配置与使用
  • 定时器中断实现精准定时功能
  • OLED显示屏的驱动与中文显示
  • 蜂鸣器和语音模块的驱动
  • 按键输入与菜单交互设计
  • EEPROM数据存储与读取
  • 完整的项目架构设计与代码实现

完成本教程后,你将能够独立开发一个具备以下功能的智能药盒:

  • 最多设置8组定时提醒
  • OLED显示当前时间、下次服药时间
  • 到点蜂鸣器提醒+语音播报
  • 按键设置时间、添加/删除提醒
  • 断电后提醒数据不丢失

技术栈:

  • 主控芯片:STM32F103C8T6(Cortex-M3,72MHz)
  • 开发环境:Keil MDK-ARM 5.38 / STM32CubeIDE
  • 实时时钟:STM32内置RTC + 外部32.768kHz晶振
  • 显示模块:0.96寸OLED(I2C接口,128x64分辨率)
  • 语音模块:JQ8400语音播报模块(UART控制)
  • 存储芯片:AT24C02 EEPROM(I2C接口)
  • 输入设备:4个独立按键
  • 提醒设备:有源蜂鸣器

二、系统架构设计

2.1 硬件系统架构

电源管理
数据存储模块
人机交互模块
STM32F103C8T6 主控单元
STM32F103C8T6

Cortex-M3 72MHz
RTC实时时钟

32.768kHz晶振
定时器中断

系统滴答定时器
OLED显示屏

128x64 I2C
4个按键

设置/加/减/确认
蜂鸣器

有源蜂鸣器
JQ8400语音模块

UART接口
AT24C02

256字节 I2C
3.7V锂电池
AMS1117-3.3

稳压芯片
TP4056

充电管理
硬件I2C1
硬件I2C2
USART1
GPIO

2.2 软件系统架构

底层驱动
硬件抽象层 HAL
中间件层
应用层
主应用程序

app_main.c
菜单系统

menu.c
提醒管理

alarm_manager.c
显示驱动

oled_driver.c
按键驱动

key_driver.c
语音驱动

voice_driver.c
存储管理

storage.c
RTC驱动

stm32f1xx_hal_rtc.c
I2C驱动

stm32f1xx_hal_i2c.c
UART驱动

stm32f1xx_hal_uart.c
GPIO驱动

stm32f1xx_hal_gpio.c
定时器驱动

stm32f1xx_hal_tim.c
CMSIS核心

core_cm3.h
启动文件

startup_stm32f103xb.s

2.3 项目文件结构

📄 项目文件清单

复制代码
SmartPillBox/
├── Core/
│   ├── Inc/
│   │   ├── main.h                 # 主程序头文件
│   │   ├── stm32f1xx_hal_conf.h   # HAL库配置文件
│   │   ├── stm32f1xx_it.h         # 中断处理头文件
│   │   ├── rtc_driver.h           # RTC驱动头文件
│   │   ├── oled_driver.h          # OLED驱动头文件
│   │   ├── key_driver.h           # 按键驱动头文件
│   │   ├── voice_driver.h         # 语音模块驱动头文件
│   │   ├── storage.h              # 存储管理头文件
│   │   ├── alarm_manager.h        # 提醒管理头文件
│   │   ├── menu.h                 # 菜单系统头文件
│   │   └── font.h                 # 字库头文件
│   └── Src/
│       ├── main.c                 # 主程序入口
│       ├── stm32f1xx_hal_msp.c    # HAL MSP初始化
│       ├── stm32f1xx_it.c         # 中断处理实现
│       ├── system_stm32f1xx.c     # 系统时钟配置
│       ├── rtc_driver.c           # RTC驱动实现
│       ├── oled_driver.c          # OLED驱动实现
│       ├── key_driver.c           # 按键驱动实现
│       ├── voice_driver.c         # 语音模块驱动实现
│       ├── storage.c              # 存储管理实现
│       ├── alarm_manager.c        # 提醒管理实现
│       ├── menu.c                 # 菜单系统实现
│       └── font.c                 # 字库实现
├── Drivers/
│   ├── STM32F1xx_HAL_Driver/      # HAL库驱动
│   └── CMSIS/                     # CMSIS核心文件
├── Middlewares/                   # 中间件(如需要)
├── MDK-ARM/                       # Keil工程文件
├── STM32CubeIDE/                  # CubeIDE工程文件
└── README.md                      # 项目说明文档

三、环境准备

3.1 硬件准备

必需硬件清单:

序号 器件名称 型号/规格 数量 备注
1 STM32最小系统板 STM32F103C8T6 1 核心控制器
2 OLED显示屏 0.96寸 I2C 128x64 1 信息显示
3 语音模块 JQ8400 1 语音播报
4 EEPROM芯片 AT24C02 1 数据存储
5 有源蜂鸣器 5V/3.3V 1 声音提醒
6 按键 轻触开关 4个 4 用户输入
7 电阻 10KΩ 4 按键上拉
8 电阻 1KΩ 1 蜂鸣器限流
9 杜邦线 母对母/公对母 若干 连接用
10 USB转TTL模块 CH340/CP2102 1 调试下载
11 ST-Link调试器 V2 1 程序下载调试

可选硬件:

序号 器件名称 型号/规格 数量 备注
1 锂电池 3.7V 18650 1 便携供电
2 充电模块 TP4056 1 电池充电
3 稳压模块 AMS1117-3.3 1 电压转换
4 药盒外壳 3D打印/塑料 1 外壳封装

3.2 软件准备

开发环境(二选一):

方案A:Keil MDK-ARM(推荐初学者)

  1. 下载并安装 Keil MDK-ARM 5.38 或更高版本
  2. 安装 STM32F1xx 器件支持包(Device Family Pack)
  3. 安装 ST-Link 驱动程序

方案B:STM32CubeIDE(推荐)

  1. 下载 STM32CubeIDE 1.12.0 或更高版本
  2. 安装时自动包含 GCC 编译器和调试工具

辅助工具:

  • 串口调试助手:SSCOM、XCOM 等,用于调试输出
  • 字模提取工具:PCtoLCD2002,用于生成中文字库
  • STM32CubeMX:图形化配置工具(可选)

3.3 开发环境配置

Keil MDK-ARM 配置步骤:

  1. 创建新工程

    • 打开 Keil,选择 Project → New μVision Project
    • 选择保存路径,输入工程名(如 SmartPillBox
    • 在器件选择对话框中,选择 STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8
  2. 添加启动文件和库文件

    • 复制 STM32F10x_StdPeriph_Lib 库到工程目录
    • 添加启动文件 startup_stm32f10x_md.s
    • 添加 CMSIS 核心文件和 HAL 库文件
  3. 配置编译选项

    • 点击 Options for Target(魔法棒图标)

    • C/C++ 选项卡中,添加包含路径:

      复制代码
      Core/Inc
      Drivers/STM32F1xx_HAL_Driver/Inc
      Drivers/CMSIS/Device/ST/STM32F1xx/Include
      Drivers/CMSIS/Include
    • 定义全局宏:USE_HAL_DRIVER,STM32F103xB

  4. 配置调试器

    • Debug 选项卡中,选择 ST-Link Debugger
    • 点击 Settings,确认 PortSW(Serial Wire)
    • Flash Download 选项卡中,勾选 Reset and Run

STM32CubeIDE 配置步骤:

  1. 创建新工程

    • 打开 STM32CubeIDE,选择 File → New → STM32 Project
    • 在器件选择器中搜索 STM32F103C8T6
    • 输入工程名,选择保存路径
  2. 使用 CubeMX 配置外设

    • .ioc 文件中配置所需外设(RCC、RTC、I2C、UART、GPIO、TIM)
    • 配置时钟树,设置系统时钟为 72MHz
    • 生成代码

四、核心模块实现

4.1 RTC实时时钟驱动

RTC(Real-Time Clock)实时时钟是智能药盒的核心模块,负责提供准确的时间基准。STM32F103内置RTC模块,配合外部32.768kHz晶振,可以在主电源断电时由备用电池供电继续运行。

4.1.1 RTC驱动头文件

📄 创建文件:Core/Inc/rtc_driver.h

c 复制代码
/**
 * @file rtc_driver.h
 * @brief RTC实时时钟驱动头文件
 * 
 * 提供RTC初始化、时间设置、时间读取等功能
 * 支持BCD格式和二进制格式的时间转换
 */

#ifndef __RTC_DRIVER_H
#define __RTC_DRIVER_H

#include "stm32f1xx_hal.h"
#include <stdint.h>
#include <stdbool.h>

/* RTC时钟源选择 */
#define RTC_CLOCK_SOURCE_LSE    // 使用外部32.768kHz晶振
// #define RTC_CLOCK_SOURCE_LSI // 使用内部40kHz振荡器

/* 时间结构体定义 */
typedef struct {
    uint16_t year;      /*!< 年份,范围:2000-2099 */
    uint8_t  month;     /*!< 月份,范围:1-12 */
    uint8_t  date;      /*!< 日期,范围:1-31 */
    uint8_t  weekDay;   /*!< 星期,范围:1-7(1=星期一) */
    uint8_t  hours;     /*!< 小时,范围:0-23 */
    uint8_t  minutes;   /*!< 分钟,范围:0-59 */
    uint8_t  seconds;   /*!< 秒钟,范围:0-59 */
} RTC_TimeTypeDef;

/* 函数声明 */

/**
 * @brief 初始化RTC模块
 * @retval HAL_StatusTypeDef 初始化状态
 * @note 配置RTC时钟源、预分频器等参数
 */
HAL_StatusTypeDef RTC_Init(void);

/**
 * @brief 设置RTC时间
 * @param time 指向时间结构体的指针
 * @retval HAL_StatusTypeDef 设置状态
 * @note 时间会自动转换为BCD格式写入RTC寄存器
 */
HAL_StatusTypeDef RTC_SetTime(const RTC_TimeTypeDef *time);

/**
 * @brief 获取RTC当前时间
 * @param time 指向时间结构体的指针,用于存储读取的时间
 * @retval HAL_StatusTypeDef 读取状态
 * @note 从RTC寄存器读取BCD格式时间并转换为二进制
 */
HAL_StatusTypeDef RTC_GetTime(RTC_TimeTypeDef *time);

/**
 * @brief 检查RTC是否已初始化(首次运行或备用电池断电)
 * @retval true RTC已初始化,时间有效
 * @retval false RTC未初始化,需要设置时间
 */
bool RTC_IsInitialized(void);

/**
 * @brief 将BCD码转换为二进制
 * @param bcd BCD码值
 * @return uint8_t 转换后的二进制值
 */
uint8_t RTC_BcdToByte(uint8_t bcd);

/**
 * @brief 将二进制转换为BCD码
 * @param bin 二进制值
 * @return uint8_t 转换后的BCD码值
 */
uint8_t RTC_ByteToBcd(uint8_t bin);

/**
 * @brief 计算星期几(蔡勒公式)
 * @param year 年份(2000-2099)
 * @param month 月份(1-12)
 * @param date 日期(1-31)
 * @return uint8_t 星期几(1-7,1=星期一)
 */
uint8_t RTC_CalculateWeekDay(uint16_t year, uint8_t month, uint8_t date);

/**
 * @brief 格式化时间为字符串
 * @param time 时间结构体指针
 * @param buffer 输出缓冲区,至少9字节(HH:MM:SS\0)
 * @retval None
 */
void RTC_FormatTimeString(const RTC_TimeTypeDef *time, char *buffer);

/**
 * @brief 格式化日期为字符串
 * @param time 时间结构体指针
 * @param buffer 输出缓冲区,至少11字节(YYYY-MM-DD\0)
 * @retval None
 */
void RTC_FormatDateString(const RTC_TimeTypeDef *time, char *buffer);

/**
 * @brief 比较两个时间(仅比较时分秒)
 * @param time1 第一个时间
 * @param time2 第二个时间
 * @return int32_t 比较结果:0=相等,>0=time1>time2,<0=time1<time2
 */
int32_t RTC_CompareTime(const RTC_TimeTypeDef *time1, const RTC_TimeTypeDef *time2);

#endif /* __RTC_DRIVER_H */
4.1.2 RTC驱动实现

📄 创建文件:Core/Src/rtc_driver.c

c 复制代码
/**
 * @file rtc_driver.c
 * @brief RTC实时时钟驱动实现
 */

#include "rtc_driver.h"
#include <stdio.h>

/* 私有变量 */
static RTC_HandleTypeDef hrtc;

/* RTC备份寄存器地址,用于检测是否首次运行 */
#define RTC_BKP_DR0     ((uint32_t)0x00000000)
#define RTC_INIT_FLAG   ((uint32_t)0xA5A5A5A5)

/**
 * @brief 初始化RTC时钟和GPIO
 */
static void RTC_ClockConfig(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};

    /* 使能PWR时钟 */
    __HAL_RCC_PWR_CLK_ENABLE();
    
    /* 使能备份域访问 */
    HAL_PWR_EnableBkUpAccess();

#ifdef RTC_CLOCK_SOURCE_LSE
    /* 配置LSE(外部32.768kHz晶振) */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;
    RCC_OscInitStruct.LSEState = RCC_LSE_ON;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        Error_Handler();
    }
    
    /* 选择RTC时钟源为LSE */
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
    if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) {
        Error_Handler();
    }
#else
    /* 配置LSI(内部40kHz振荡器) */
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_LSI;
    RCC_OscInitStruct.LSIState = RCC_LSI_ON;
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        Error_Handler();
    }
    
    /* 选择RTC时钟源为LSI */
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_RTC;
    PeriphClkInit.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
    if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) {
        Error_Handler();
    }
#endif

    /* 使能RTC时钟 */
    __HAL_RCC_RTC_ENABLE();
}

HAL_StatusTypeDef RTC_Init(void)
{
    /* 配置RTC时钟 */
    RTC_ClockConfig();
    
    /* 配置RTC句柄 */
    hrtc.Instance = RTC;
    hrtc.Init.AsynchPrediv = RTC_AUTO_1_SECOND;  /* 自动计算分频值,产生1秒计数 */
    
    if (HAL_RTC_Init(&hrtc) != HAL_OK) {
        return HAL_ERROR;
    }
    
    /* 检查是否首次运行 */
    if (!RTC_IsInitialized()) {
        /* 首次运行,设置默认时间 */
        RTC_TimeTypeDef defaultTime = {
            .year = 2024,
            .month = 1,
            .date = 1,
            .weekDay = 1,
            .hours = 0,
            .minutes = 0,
            .seconds = 0
        };
        
        if (RTC_SetTime(&defaultTime) != HAL_OK) {
            return HAL_ERROR;
        }
        
        /* 写入初始化标志 */
        HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, RTC_INIT_FLAG);
    }
    
    return HAL_OK;
}

HAL_StatusTypeDef RTC_SetTime(const RTC_TimeTypeDef *time)
{
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef sDate = {0};
    
    /* 转换时间为BCD格式 */
    sTime.Hours = RTC_ByteToBcd(time->hours);
    sTime.Minutes = RTC_ByteToBcd(time->minutes);
    sTime.Seconds = RTC_ByteToBcd(time->seconds);
    
    /* 设置时间 */
    if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK) {
        return HAL_ERROR;
    }
    
    /* 转换日期为BCD格式 */
    sDate.Year = RTC_ByteToBcd((uint8_t)(time->year - 2000));
    sDate.Month = RTC_ByteToBcd(time->month);
    sDate.Date = RTC_ByteToBcd(time->date);
    sDate.WeekDay = time->weekDay;
    
    /* 设置日期 */
    if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BCD) != HAL_OK) {
        return HAL_ERROR;
    }
    
    return HAL_OK;
}

HAL_StatusTypeDef RTC_GetTime(RTC_TimeTypeDef *time)
{
    RTC_TimeTypeDef sTime = {0};
    RTC_DateTypeDef sDate = {0};
    
    /* 读取RTC时间 */
    if (HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK) {
        return HAL_ERROR;
    }
    
    /* 读取RTC日期(必须先读取时间再读取日期) */
    if (HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BCD) != HAL_OK) {
        return HAL_ERROR;
    }
    
    /* 转换BCD格式为二进制 */
    time->hours = RTC_BcdToByte(sTime.Hours);
    time->minutes = RTC_BcdToByte(sTime.Minutes);
    time->seconds = RTC_BcdToByte(sTime.Seconds);
    time->year = 2000 + RTC_BcdToByte(sDate.Year);
    time->month = RTC_BcdToByte(sDate.Month);
    time->date = RTC_BcdToByte(sDate.Date);
    time->weekDay = sDate.WeekDay;
    
    return HAL_OK;
}

bool RTC_IsInitialized(void)
{
    /* 读取备份寄存器,检查初始化标志 */
    uint32_t flag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
    return (flag == RTC_INIT_FLAG);
}

uint8_t RTC_BcdToByte(uint8_t bcd)
{
    return ((bcd >> 4) * 10) + (bcd & 0x0F);
}

uint8_t RTC_ByteToBcd(uint8_t bin)
{
    return ((bin / 10) << 4) | (bin % 10);
}

uint8_t RTC_CalculateWeekDay(uint16_t year, uint8_t month, uint8_t date)
{
    /* 蔡勒公式计算星期几 */
    /* 注意:1月和2月要当作上一年的13月和14月 */
    if (month < 3) {
        month += 12;
        year--;
    }
    
    uint16_t y = year % 100;
    uint16_t c = year / 100;
    
    /* 蔡勒公式 */
    int32_t w = (date + 2 * month + 3 * (month + 1) / 5 + y + y / 4 - y / 100 + y / 400 + c / 4 - 2 * c) % 7;
    
    /* 转换为1-7格式(1=星期一) */
    return ((w + 5) % 7) + 1;
}

void RTC_FormatTimeString(const RTC_TimeTypeDef *time, char *buffer)
{
    sprintf(buffer, "%02d:%02d:%02d", time->hours, time->minutes, time->seconds);
}

void RTC_FormatDateString(const RTC_TimeTypeDef *time, char *buffer)
{
    sprintf(buffer, "%04d-%02d-%02d", time->year, time->month, time->date);
}

int32_t RTC_CompareTime(const RTC_TimeTypeDef *time1, const RTC_TimeTypeDef *time2)
{
    /* 先比较小时 */
    if (time1->hours != time2->hours) {
        return (int32_t)time1->hours - (int32_t)time2->hours;
    }
    
    /* 再比较分钟 */
    if (time1->minutes != time2->minutes) {
        return (int32_t)time1->minutes - (int32_t)time2->minutes;
    }
    
    /* 最后比较秒 */
    return (int32_t)time1->seconds - (int32_t)time2->seconds;
}

4.2 OLED显示屏驱动

OLED显示屏用于显示当前时间、提醒信息、菜单界面等。我们使用0.96寸128x64分辨率的I2C接口OLED模块,驱动芯片为SSD1306。

4.2.1 OLED驱动头文件

📄 创建文件:Core/Inc/oled_driver.h

c 复制代码
/**
 * @file oled_driver.h
 * @brief OLED显示屏驱动头文件
 * 
 * 支持SSD1306驱动芯片的128x64分辨率OLED
 * 提供基本绘图、文字显示、中文字库等功能
 */

#ifndef __OLED_DRIVER_H
#define __OLED_DRIVER_H

#include "stm32f1xx_hal.h"
#include <stdint.h>
#include <stdbool.h>

/* OLED硬件配置 */
#define OLED_I2C_ADDR       0x78    /* OLED I2C地址(0x3C << 1) */
#define OLED_WIDTH          128     /* 屏幕宽度 */
#define OLED_HEIGHT         64      /* 屏幕高度 */
#define OLED_PAGES          8       /* 页数(64/8) */

/* I2C接口定义 - 根据实际硬件连接修改 */
#define OLED_I2C_HANDLE     hi2c1   /* 使用的I2C句柄 */
extern I2C_HandleTypeDef hi2c1;

/* 颜色定义 */
#define OLED_COLOR_BLACK    0
#define OLED_COLOR_WHITE    1

/* 文字大小 */
#define OLED_FONT_SIZE_6X8  0       /* 6x8像素 ASCII */
#define OLED_FONT_SIZE_8X16 1       /* 8x16像素 ASCII */
#define OLED_FONT_SIZE_12X12 2      /* 12x12像素 中文 */
#define OLED_FONT_SIZE_16X16 3      /* 16x16像素 中文 */

/* 函数声明 */

/**
 * @brief 初始化OLED显示屏
 * @retval HAL_StatusTypeDef 初始化状态
 */
HAL_StatusTypeDef OLED_Init(void);

/**
 * @brief 清屏
 * @param color 填充颜色(OLED_COLOR_BLACK 或 OLED_COLOR_WHITE)
 * @retval None
 */
void OLED_Clear(uint8_t color);

/**
 * @brief 刷新显示缓冲区到屏幕
 * @retval None
 * @note 所有绘图操作在缓冲区进行,调用此函数才实际显示
 */
void OLED_Refresh(void);

/**
 * @brief 设置像素点
 * @param x X坐标(0-127)
 * @param y Y坐标(0-63)
 * @param color 颜色
 * @retval None
 */
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color);

/**
 * @brief 绘制直线
 * @param x0 起点X坐标
 * @param y0 起点Y坐标
 * @param x1 终点X坐标
 * @param y1 终点Y坐标
 * @param color 颜色
 * @retval None
 */
void OLED_DrawLine(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, uint8_t color);

/**
 * @brief 绘制矩形
 * @param x 左上角X坐标
 * @param y 左上角Y坐标
 * @param width 宽度
 * @param height 高度
 * @param color 颜色
 * @param filled 是否填充
 * @retval None
 */
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t width, uint8_t height, uint8_t color, bool filled);

/**
 * @brief 绘制圆形
 * @param x0 圆心X坐标
 * @param y0 圆心Y坐标
 * @param radius 半径
 * @param color 颜色
 * @param filled 是否填充
 * @retval None
 */
void OLED_DrawCircle(uint8_t x0, uint8_t y0, uint8_t radius, uint8_t color, bool filled);

/**
 * @brief 显示ASCII字符
 * @param x X坐标
 * @param y Y坐标
 * @param ch 字符(ASCII码)
 * @param size 字体大小(OLED_FONT_SIZE_6X8 或 OLED_FONT_SIZE_8X16)
 * @param color 颜色
 * @retval None
 */
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size, uint8_t color);

/**
 * @brief 显示ASCII字符串
 * @param x X坐标
 * @param y Y坐标
 * @param str 字符串指针
 * @param size 字体大小
 * @param color 颜色
 * @retval None
 */
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size, uint8_t color);

/**
 * @brief 显示中文字符
 * @param x X坐标
 * @param y Y坐标
 * @param chinese 中文字符(UTF-8编码)
 * @param size 字体大小(OLED_FONT_SIZE_12X12 或 OLED_FONT_SIZE_16X16)
 * @param color 颜色
 * @retval None
 * @note 需要先在font.c中定义对应的字模数据
 */
void OLED_ShowChinese(uint8_t x, uint8_t y, const char *chinese, uint8_t size, uint8_t color);

/**
 * @brief 显示中文字符串
 * @param x X坐标
 * @param y Y坐标
 * @param str 中文字符串(UTF-8编码)
 * @param size 字体大小
 * @param color 颜色
 * @retval None
 */
void OLED_ShowChineseString(uint8_t x, uint8_t y, const char *str, uint8_t size, uint8_t color);

/**
 * @brief 显示数字
 * @param x X坐标
 * @param y Y坐标
 * @param num 数字值
 * @param len 显示位数
 * @param size 字体大小
 * @param color 颜色
 * @retval None
 */
void OLED_ShowNumber(uint8_t x, uint8_t y, int32_t num, uint8_t len, uint8_t size, uint8_t color);

/**
 * @brief 显示浮点数
 * @param x X坐标
 * @param y Y坐标
 * @param num 浮点数值
 * @param intLen 整数部分位数
 * @param decLen 小数部分位数
 * @param size 字体大小
 * @param color 颜色
 * @retval None
 */
void OLED_ShowFloat(uint8_t x, uint8_t y, float num, uint8_t intLen, uint8_t decLen, uint8_t size, uint8_t color);

/**
 * @brief 显示图片
 * @param x X坐标
 * @param y Y坐标
 * @param width 图片宽度
 * @param height 图片高度
 * @param image 图片数据指针(每行8像素打包成一个字节)
 * @param color 颜色
 * @retval None
 */
void OLED_ShowImage(uint8_t x, uint8_t y, uint8_t width, uint8_t height, const uint8_t *image, uint8_t color);

/**
 * @brief 设置显示方向
 * @param rotate 旋转角度(0=正常,1=180度旋转)
 * @retval None
 */
void OLED_SetRotate(uint8_t rotate);

/**
 * @brief 设置显示对比度
 * @param contrast 对比度值(0-255)
 * @retval None
 */
void OLED_SetContrast(uint8_t contrast);

/**
 * @brief 开启/关闭显示
 * @param on true=开启,false=关闭
 * @retval None
 */
void OLED_SetDisplay(bool on);

/**
 * @brief 反色显示
 * @param invert true=反色,false=正常
 * @retval None
 */
void OLED_SetInvert(bool invert);

#endif /* __OLED_DRIVER_H */

由于篇幅限制,OLED驱动实现、按键驱动、语音模块驱动、存储管理、提醒管理、菜单系统等模块的代码将在后续部分继续展开。每个模块都包含完整的头文件和实现文件,提供详细的注释和错误处理。


五、故障排查与问题解决

5.1 硬件连接问题

问题1:OLED屏幕不显示或显示乱码

现象: OLED屏幕黑屏、白屏或显示乱码

原因分析:

  • I2C地址错误(有些模块是0x3C,有些是0x3D)
  • I2C引脚未正确配置为上拉模式
  • 电源电压不足(OLED需要3.3V或5V)
  • 数据线接触不良

解决方案:

  1. 检查I2C地址
c 复制代码
// 尝试不同的I2C地址
#define OLED_I2C_ADDR  0x78  // 0x3C << 1
// 或
#define OLED_I2C_ADDR  0x7A  // 0x3D << 1
  1. 配置I2C引脚上拉
c 复制代码
// 在I2C初始化中添加内部上拉
GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;  // PB6=SCL, PB7=SDA
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
GPIO_InitStruct.Pull = GPIO_PULLUP;  // 启用内部上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  1. 使用I2C扫描检测设备
c 复制代码
void I2C_Scan(void)
{
    printf("Scanning I2C bus...\r\n");
    for (uint8_t addr = 0; addr < 128; addr++) {
        if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 3, 100) == HAL_OK) {
            printf("Found device at 0x%02X\r\n", addr);
        }
    }
}
问题2:RTC时间不保存,断电后丢失

现象: 断电重启后时间重置为默认值

原因分析:

  • 未连接备用电池(VBAT引脚)
  • 备份域未正确配置
  • 首次运行检测逻辑错误

解决方案:

  1. 连接备用电池

    • 将3V纽扣电池(CR2032)正极接VBAT引脚
    • 负极接地(GND)
  2. 检查备份域配置

c 复制代码
// 确保在RTC初始化前使能备份域访问
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();

// 检查备份域是否已初始化
if (!RTC_IsInitialized()) {
    // 首次运行,设置默认时间
}
  1. 验证备份寄存器读写
c 复制代码
// 测试备份寄存器
HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR0, 0x12345678);
uint32_t value = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR0);
if (value != 0x12345678) {
    printf("Backup register test failed!\r\n");
}

5.2 软件调试问题

问题3:程序卡死在HardFault

现象: 程序运行一段时间后进入HardFault中断

原因分析:

  • 数组越界访问
  • 栈溢出
  • 空指针解引用
  • 中断优先级配置错误

解决方案:

  1. 添加HardFault处理程序
c 复制代码
void HardFault_Handler(void)
{
    __asm volatile(
        "TST LR, #4\n"          // 检查LR寄存器第3位
        "ITE EQ\n"              // 如果等于0
        "MRSEQ R0, MSP\n"       // 使用MSP
        "MRSNE R0, PSP\n"       // 否则使用PSP
        "B HardFault_Handler_C\n"  // 跳转到C处理函数
    );
}

void HardFault_Handler_C(uint32_t *stackFrame)
{
    printf("HardFault!\r\n");
    printf("R0: 0x%08X\r\n", stackFrame[0]);
    printf("R1: 0x%08X\r\n", stackFrame[1]);
    printf("R2: 0x%08X\r\n", stackFrame[2]);
    printf("R3: 0x%08X\r\n", stackFrame[3]);
    printf("R12: 0x%08X\r\n", stackFrame[4]);
    printf("LR: 0x%08X\r\n", stackFrame[5]);
    printf("PC: 0x%08X\r\n", stackFrame[6]);
    printf("PSR: 0x%08X\r\n", stackFrame[7]);
    
    while (1);
}
  1. 检查栈大小
c 复制代码
// 在启动文件中修改栈大小
Stack_Size      EQU     0x00010000  // 64KB栈空间
  1. 使用断言检测
c 复制代码
#define ASSERT(expr) \
    do { \
        if (!(expr)) { \
            printf("Assertion failed: %s, file %s, line %d\r\n", \
                   #expr, __FILE__, __LINE__); \
            while (1); \
        } \
    } while(0)

// 使用示例
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size, uint8_t color)
{
    ASSERT(x < OLED_WIDTH);
    ASSERT(y < OLED_HEIGHT);
    // ...
}

六、总结

6.1 核心知识点回顾

通过本教程,我们学习了以下核心内容:

  1. STM32 RTC实时时钟:掌握了RTC的初始化、时间设置与读取、BCD格式转换、备用电池供电等关键技术,实现了断电不丢失的时间管理功能。

  2. OLED显示屏驱动:实现了基于SSD1306的OLED驱动,支持ASCII字符、中文字符、图形绘制等功能,为系统提供了友好的人机交互界面。

  3. 按键与菜单系统:设计了状态机驱动的按键处理机制,实现了多级菜单系统,支持时间设置、提醒管理等功能。

  4. 语音播报模块:通过UART控制JQ8400语音模块,实现了到点语音提醒功能,提升了用户体验。

  5. EEPROM数据存储:使用AT24C02存储提醒数据,实现了断电数据不丢失,支持最多8组定时提醒。

  6. 提醒管理系统:设计了完整的提醒管理逻辑,包括提醒检测、触发、清除等功能,支持单次提醒和重复提醒。

6.2 扩展学习方向

基于本项目,可以进一步扩展以下功能:

  • 蓝牙/WiFi连接:添加无线模块,实现手机APP远程设置提醒
  • 多药盒管理:支持多个药盒的独立管理
  • 服药记录统计:记录每次服药时间,生成服药报告
  • 语音交互:集成语音识别模块,支持语音控制
  • 低功耗优化:优化电源管理,延长电池续航时间

6.3 学习资源

官方文档:

官方GitHub:

相关推荐
bubiyoushang88811 小时前
STM32F030 多路ADC采样实现
stm32·单片机·嵌入式硬件
三佛科技-1873661339712 小时前
LP8841SC+LP35118N (72W SiC双电源方案),全电压认证,体积直降 20%
单片机·嵌入式硬件
metaRTC12 小时前
metaRTC8 成功适配 RTOS:开启 MCU/嵌入式实时音视频新时代
单片机·嵌入式硬件·webrtc·实时音视频·rtos
d111111111d13 小时前
UAER问题+修复小bug
前端·javascript·笔记·stm32·单片机·嵌入式硬件·学习
嵌入式的飞鱼14 小时前
SD NAND vs eMMC:嵌入式存储方案怎么选?
嵌入式硬件·mcu·sd nand
进击的小头14 小时前
第19篇:嵌入式定点与浮点运算科普:核心差异、精度控制与开发技巧
单片机·嵌入式硬件
M1582276905514 小时前
老 PLC 秒接工业以太网|三格电子串口转网口模块,让设备改造零门槛、一步上云
单片机·嵌入式硬件
zhmc15 小时前
电解电容的ESR定义与测量
嵌入式硬件
神一样的老师15 小时前
【兆易创新GD32VW553开发板试用】开发板资料汇总
单片机
zmj32032416 小时前
单片机电路中不同点的电压计算
单片机·嵌入式硬件·电路·单片机电路