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:

相关推荐
ACP广源盛1392462567318 小时前
GSV2231@ACP# 增强型 8K MST 多屏扩展芯片
大数据·人工智能·分布式·嵌入式硬件
国科安芯1 天前
航天器多路并联大功率电源系统设计与ASP4644均流特性分析
单片机·嵌入式硬件·fpga开发·安全性测试
茯苓gao1 天前
嵌入式开发笔记:CANopen相关移位运算与通信协议术语详解
网络·嵌入式硬件·学习·信息与通信
梁朝辉1 天前
STM32H750VBT6中ADCINP与INN什么区别
stm32·单片机·嵌入式硬件
省四收割者1 天前
一文详解信号完整性(1)
python·嵌入式硬件·数学建模·信息与通信·信号处理·智能硬件
崇山峻岭之间2 天前
单片机无感FOC驱动之ADC
单片机·嵌入式硬件
Ligocious2 天前
1.点亮一颗小小的LED
单片机·嵌入式硬件
KaifuZeng2 天前
通信与接口协议面试八、CAN通信
单片机·嵌入式硬件·面试·通信与接口协议
智源单片机设计2 天前
基于单片机的直流电机双闭环调速系统设计
单片机·嵌入式硬件
kebidaixu2 天前
两轮车换电 BMS 的模拟前端中颖 SH367306 AFE 芯片详解
stm32