文章目录
-
- 一、前言
-
- [1.1 技术背景](#1.1 技术背景)
- [1.2 应用场景](#1.2 应用场景)
- [1.3 本文目标](#1.3 本文目标)
- 二、系统架构设计
-
- [2.1 硬件系统架构](#2.1 硬件系统架构)
- [2.2 软件系统架构](#2.2 软件系统架构)
- [2.3 项目文件结构](#2.3 项目文件结构)
- 三、环境准备
-
- [3.1 硬件准备](#3.1 硬件准备)
- [3.2 软件准备](#3.2 软件准备)
- [3.3 开发环境配置](#3.3 开发环境配置)
- 四、核心模块实现
-
- [4.1 RTC实时时钟驱动](#4.1 RTC实时时钟驱动)
-
- [4.1.1 RTC驱动头文件](#4.1.1 RTC驱动头文件)
- [4.1.2 RTC驱动实现](#4.1.2 RTC驱动实现)
- [4.2 OLED显示屏驱动](#4.2 OLED显示屏驱动)
-
- [4.2.1 OLED驱动头文件](#4.2.1 OLED驱动头文件)
- 五、故障排查与问题解决
-
- [5.1 硬件连接问题](#5.1 硬件连接问题)
- [5.2 软件调试问题](#5.2 软件调试问题)
- 六、总结
-
- [6.1 核心知识点回顾](#6.1 核心知识点回顾)
- [6.2 扩展学习方向](#6.2 扩展学习方向)
- [6.3 学习资源](#6.3 学习资源)
一、前言
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(推荐初学者)
- 下载并安装 Keil MDK-ARM 5.38 或更高版本
- 安装 STM32F1xx 器件支持包(Device Family Pack)
- 安装 ST-Link 驱动程序
方案B:STM32CubeIDE(推荐)
- 下载 STM32CubeIDE 1.12.0 或更高版本
- 安装时自动包含 GCC 编译器和调试工具
辅助工具:
- 串口调试助手:SSCOM、XCOM 等,用于调试输出
- 字模提取工具:PCtoLCD2002,用于生成中文字库
- STM32CubeMX:图形化配置工具(可选)
3.3 开发环境配置
Keil MDK-ARM 配置步骤:
-
创建新工程
- 打开 Keil,选择
Project → New μVision Project - 选择保存路径,输入工程名(如
SmartPillBox) - 在器件选择对话框中,选择
STMicroelectronics → STM32F1 Series → STM32F103 → STM32F103C8
- 打开 Keil,选择
-
添加启动文件和库文件
- 复制 STM32F10x_StdPeriph_Lib 库到工程目录
- 添加启动文件
startup_stm32f10x_md.s - 添加 CMSIS 核心文件和 HAL 库文件
-
配置编译选项
-
点击
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
-
-
配置调试器
- 在
Debug选项卡中,选择ST-Link Debugger - 点击
Settings,确认Port为SW(Serial Wire) - 在
Flash Download选项卡中,勾选Reset and Run
- 在
STM32CubeIDE 配置步骤:
-
创建新工程
- 打开 STM32CubeIDE,选择
File → New → STM32 Project - 在器件选择器中搜索
STM32F103C8T6 - 输入工程名,选择保存路径
- 打开 STM32CubeIDE,选择
-
使用 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)
- 数据线接触不良
解决方案:
- 检查I2C地址
c
// 尝试不同的I2C地址
#define OLED_I2C_ADDR 0x78 // 0x3C << 1
// 或
#define OLED_I2C_ADDR 0x7A // 0x3D << 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);
- 使用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引脚)
- 备份域未正确配置
- 首次运行检测逻辑错误
解决方案:
-
连接备用电池
- 将3V纽扣电池(CR2032)正极接VBAT引脚
- 负极接地(GND)
-
检查备份域配置
c
// 确保在RTC初始化前使能备份域访问
__HAL_RCC_PWR_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
// 检查备份域是否已初始化
if (!RTC_IsInitialized()) {
// 首次运行,设置默认时间
}
- 验证备份寄存器读写
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中断
原因分析:
- 数组越界访问
- 栈溢出
- 空指针解引用
- 中断优先级配置错误
解决方案:
- 添加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);
}
- 检查栈大小
c
// 在启动文件中修改栈大小
Stack_Size EQU 0x00010000 // 64KB栈空间
- 使用断言检测
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 核心知识点回顾
通过本教程,我们学习了以下核心内容:
-
STM32 RTC实时时钟:掌握了RTC的初始化、时间设置与读取、BCD格式转换、备用电池供电等关键技术,实现了断电不丢失的时间管理功能。
-
OLED显示屏驱动:实现了基于SSD1306的OLED驱动,支持ASCII字符、中文字符、图形绘制等功能,为系统提供了友好的人机交互界面。
-
按键与菜单系统:设计了状态机驱动的按键处理机制,实现了多级菜单系统,支持时间设置、提醒管理等功能。
-
语音播报模块:通过UART控制JQ8400语音模块,实现了到点语音提醒功能,提升了用户体验。
-
EEPROM数据存储:使用AT24C02存储提醒数据,实现了断电数据不丢失,支持最多8组定时提醒。
-
提醒管理系统:设计了完整的提醒管理逻辑,包括提醒检测、触发、清除等功能,支持单次提醒和重复提醒。
6.2 扩展学习方向
基于本项目,可以进一步扩展以下功能:
- 蓝牙/WiFi连接:添加无线模块,实现手机APP远程设置提醒
- 多药盒管理:支持多个药盒的独立管理
- 服药记录统计:记录每次服药时间,生成服药报告
- 语音交互:集成语音识别模块,支持语音控制
- 低功耗优化:优化电源管理,延长电池续航时间
6.3 学习资源
官方文档:
官方GitHub: