32HAL——万年历
一、需求分析
主控:STM32F103VET6
按键 * 1 -> 切换显示模式与设置模式
旋转编码器* 1 -> 设置时间
0.96OLED * 1 -> 显示

二、硬件分析

三、代码详解
1. 初始化函数
// 初始化函数
void MainTask_Init(void) {HAL_Delay(20);OLED_Init();myRTC_Init();Encoder_Init();Knob_SetForwardCallback(onKnobForward);Knob_SetBackwardCallback(onKnobBackward);Knob_SetPressedCallback(onKnobPressed);
}
void myRTC_Init(void) {// 先获取初始化标志位uint32_t initFlag = HAL_RTCEx_BKUPRead(&hrtc, RTC_BKP_DR1);// 如果初始化标志位是我们设定的值,说明已经初始化过一次了,则直接跳出初始化if (initFlag == RTC_INIT_FLAG) return;// cubeMx生成的RTC初始化代码if (HAL_RTC_Init(&hrtc) != HAL_OK){Error_Handler();}// 如果第一次初始化则给时间赋初始值, 这里使用结构体, 跟time.h一样struct tm time = {.tm_year = 2025 - 1900, // 注意这里的年是当前年与1900的差值.tm_mon = 11 - 1, // 这里的月是0-11,所以实际月份要减1.tm_mday = 8,.tm_hour = 20,.tm_min = 12,.tm_sec = 30,};//将时间存入RTCmyRTC_SetTime(&time);//运行到这里说明是第一次初始化,所以要把设定好的初始化标志位存进BKP, 防止下次时间被重新赋值HAL_RTCEx_BKUPWrite(&hrtc, RTC_BKP_DR1, RTC_INIT_FLAG);
}
// 编码器初始化
void Encoder_Init(void) {HAL_TIM_Encoder_Start(&htim1, TIM_CHANNEL_ALL);setCounter(COUNTER_INIT_VALUE);
}
/*** @brief 设置旋钮正转回调函数* @param callback 回调函数指针*/
void Knob_SetForwardCallback(KnobCallback callback){onForwardCallback = callback;
}/*** @brief 设置旋钮反转回调函数* @param callback 回调函数指针*/
void Knob_SetBackwardCallback(KnobCallback callback){onBackwardCallback = callback;
}/*** @brief 设置旋钮按下回调函数* @param callback 回调函数指针*/
void Knob_SetPressedCallback(KnobCallback callback){onPressedCallback = callback;
}
2. RTC获取与设置时间
//设置RTC时间
HAL_StatusTypeDef myRTC_SetTime(struct tm *time) {uint32_t unixTime = mktime(time); // 将实际时间转换为时间戳return RTC_WriteTimeCounter(&hrtc, unixTime);
}//获取RTC时间
struct tm *myRTC_GetTime(void) {time_t unixTime = RTC_ReadTimeCounter(&hrtc); //将时间戳转换为实际时间return gmtime(&unixTime);
}
3. 重定义相关函数
// 设置编码器计数值
void setCounter(int value) {__HAL_TIM_SET_COUNTER(&htim1, value);
}// 读取编码器计数值
uint32_t getCounter(void) {return __HAL_TIM_GET_COUNTER(&htim1);
}// 获取按键状态
BtnState getBtnState(void) {// 读取IO电平,返回按键状态:如果低电平则为按下,否则为没按下return HAL_GPIO_ReadPin(Button_GPIO_Port, Button_Pin) == GPIO_PIN_RESET ? Pressed : Unpressed;
}// 获取系统时间
uint32_t getTick(void) {return HAL_GetTick();
}
4. 按键&旋转编码器处理函数
//旋钮&按键循环处理函数
void Encoder_loop(void) {uint32_t counter = getCounter();// 正转if (counter > COUNTER_INIT_VALUE) {// 正转回调函数if (onForwardCallback != NULL) {onForwardCallback();}}// 反转if (counter < COUNTER_INIT_VALUE) {// 反转回调函数if (onBackwardCallback != NULL) {onBackwardCallback();}}setCounter(COUNTER_INIT_VALUE);// 获取按键状态BtnState btnstate = getBtnState();// 记录按键按下时间static uint32_t pressedTime = 0;// 记录是否调用 0:未调用 1:已调用static uint8_t callbackState = 0;if (btnstate == Pressed) {if (pressedTime == 0) {pressedTime = getTick();// 防止重复检测按键按下 有效按下一次即可} else if (callbackState == 0 && getTick() - pressedTime > BTN_DEBOUNCE_TICKS) {// 按键回调函数if (onPressedCallback != NULL) {onPressedCallback();}callbackState = 1;}} else {pressedTime = 0;callbackState = 0;}
}
5. 旋转编码器正反转&按键回调函数
char weeks[7][10] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};// 万年历状态枚举
typedef enum {CalendarState_Normal,CalendarState_Setting,
} CalendarState;// 设置状态枚举
typedef enum {Year = 0, Month, Day, Hour, Minute, Second} SettingState;// 设置时间结构体
struct tm settingTime;// 当前万年历状态
CalendarState calendarState = CalendarState_Normal;// 当前设置状态
SettingState settingState = Year;// 旋钮顺时针旋转回调函数
void onKnobForward(void) {if (calendarState == CalendarState_Setting){switch (settingState){case Year:settingTime.tm_year++;break;case Month:settingTime.tm_mon++;if (settingTime.tm_mon > 11){settingTime.tm_mon = 0;}break;case Day:settingTime.tm_mday++;if (settingTime.tm_mday > 31){settingTime.tm_mday = 1;}break;case Hour:settingTime.tm_hour++;if (settingTime.tm_hour > 23){settingTime.tm_hour = 0;}break;case Minute:settingTime.tm_min++;if (settingTime.tm_min > 59){settingTime.tm_min = 0;}break;case Second:settingTime.tm_sec++;if (settingTime.tm_sec > 59){settingTime.tm_sec = 0;}break;}}
}// 旋钮逆时针旋转回调函数
void onKnobBackward(void) {if (calendarState == CalendarState_Setting){switch (settingState){case Year:settingTime.tm_year--;if (settingTime.tm_year < 70){settingTime.tm_year = 70;}break;case Month:settingTime.tm_mon--;if (settingTime.tm_mon < 0){settingTime.tm_mon = 11;}break;case Day:settingTime.tm_mday--;if (settingTime.tm_mday < 0){settingTime.tm_mday = 31;}break;case Hour:settingTime.tm_hour--;if (settingTime.tm_hour < 0){settingTime.tm_hour = 23;}break;case Minute:settingTime.tm_min--;if (settingTime.tm_min < 0){settingTime.tm_min = 59;}break;case Second:settingTime.tm_sec--;if (settingTime.tm_sec < 0){settingTime.tm_sec = 59;}break;}}
}// 按键按下回调函数
void onKnobPressed(void) {if (calendarState == CalendarState_Normal) {settingTime = *myRTC_GetTime();settingState = Year;calendarState = CalendarState_Setting;} else {if (settingState == Second) {myRTC_SetTime(&settingTime);calendarState = CalendarState_Normal;} else {settingState ++;}}
}
6. 显示函数—光标&时间
// 时间显示函数
void showTime(struct tm* time) {char str[50];sprintf(str, "%d-%02d-%02d", time->tm_year + 1900, time->tm_mon + 1, time->tm_mday);OLED_PrintASCIIString(24, 0, str, &afont16x8, OLED_COLOR_NORMAL);sprintf(str, "%02d:%02d:%02d", time->tm_hour, time->tm_min, time->tm_sec);OLED_PrintASCIIString(16, 20, str, &afont24x12, OLED_COLOR_NORMAL);char *week = weeks[time->tm_wday];uint8_t x_week = (128 - strlen(week) * 8) / 2;OLED_PrintASCIIString(x_week, 48, week, &afont16x8, OLED_COLOR_NORMAL);
}// 显示光标函数
void showCursor(){static uint32_t startTime = 0;uint32_t difftime = HAL_GetTick() - startTime;if (difftime > 2 * CURSOR_FLASH_INTERVAL){startTime = HAL_GetTick();}else if (difftime > CURSOR_FLASH_INTERVAL){CursorPosition position = cursorPositions[settingState];OLED_DrawLine(position.x1, position.y1, position.x2, position.y2, OLED_COLOR_NORMAL);}
}
7. 任务处理函数
// 任务处理
void MainTask(void) {Encoder_loop();OLED_NewFrame();if (calendarState == CalendarState_Normal) {struct tm* now = myRTC_GetTime();showTime(now);} else {showTime(&settingTime);showCursor();}OLED_ShowFrame();
}
四、cubeMx配置
1. 时钟树
RTC使用外部低速晶振LSE 32.768Hz

2. 按键PE15为输入模式

3. RTC

4. 旋转编码器

5. I2C

6. USART

五、函数指针详解
1. 定义函数指针
typedef void (*KnobCallback)(void);
2. 存储回调函数地址&实际被调用函数
/** 旋钮正转回调函数指针 */
KnobCallback onForwardCallback = NULL;
/** 旋钮反转回调函数指针 */
KnobCallback onBackwardCallback = NULL;
/** 旋钮按下回调函数指针 */
KnobCallback onPressedCallback = NULL;
3. 回调函数注册接口&实际就是将我们写的应用层回调函数地址传递给上述实际被调用函数
/*** @brief 设置旋钮正转回调函数* @param callback 回调函数指针*/
void Knob_SetForwardCallback(KnobCallback callback){onForwardCallback = callback;
}/*** @brief 设置旋钮反转回调函数* @param callback 回调函数指针*/
void Knob_SetBackwardCallback(KnobCallback callback){onBackwardCallback = callback;
}/*** @brief 设置旋钮按下回调函数* @param callback 回调函数指针*/
void Knob_SetPressedCallback(KnobCallback callback){onPressedCallback = callback;
}
4. 应用层回调函数
// 旋钮顺时针旋转回调函数
void onKnobForward(void) {if (calendarState == CalendarState_Setting){switch (settingState){case Year:settingTime.tm_year++;break;case Month:settingTime.tm_mon++;if (settingTime.tm_mon > 11){settingTime.tm_mon = 0;}break;case Day:settingTime.tm_mday++;if (settingTime.tm_mday > 31){settingTime.tm_mday = 1;}break;case Hour:settingTime.tm_hour++;if (settingTime.tm_hour > 23){settingTime.tm_hour = 0;}break;case Minute:settingTime.tm_min++;if (settingTime.tm_min > 59){settingTime.tm_min = 0;}break;case Second:settingTime.tm_sec++;if (settingTime.tm_sec > 59){settingTime.tm_sec = 0;}break;}}
}// 旋钮逆时针旋转回调函数
void onKnobBackward(void) {if (calendarState == CalendarState_Setting){switch (settingState){case Year:settingTime.tm_year--;if (settingTime.tm_year < 70){settingTime.tm_year = 70;}break;case Month:settingTime.tm_mon--;if (settingTime.tm_mon < 0){settingTime.tm_mon = 11;}break;case Day:settingTime.tm_mday--;if (settingTime.tm_mday < 0){settingTime.tm_mday = 31;}break;case Hour:settingTime.tm_hour--;if (settingTime.tm_hour < 0){settingTime.tm_hour = 23;}break;case Minute:settingTime.tm_min--;if (settingTime.tm_min < 0){settingTime.tm_min = 59;}break;case Second:settingTime.tm_sec--;if (settingTime.tm_sec < 0){settingTime.tm_sec = 59;}break;}}
}// 按键按下回调函数
void onKnobPressed(void) {if (calendarState == CalendarState_Normal) {settingTime = *myRTC_GetTime();settingState = Year;calendarState = CalendarState_Setting;} else {if (settingState == Second) {myRTC_SetTime(&settingTime);calendarState = CalendarState_Normal;} else {settingState ++;}}
}
5. 初始化
Knob_SetForwardCallback(onKnobForward);Knob_SetBackwardCallback(onKnobBackward);Knob_SetPressedCallback(onKnobPressed);
6. 驱动层调用
// 正转回调函数
if (onForwardCallback != NULL) {onForwardCallback();
}// 反转回调函数
if (onBackwardCallback != NULL) {onBackwardCallback();
}// 按键回调函数
if (onPressedCallback != NULL) {onPressedCallback();
}
注:上述代码原作者:keysking
