feat: 实现智能按摩器完整功能 (定时/语音/电机/显示)

- 新增MP3语音模块驱动,集成18个语音提示,支持播放/停止/音量调节
- 实现0/10/20/30分钟循环定时倒计时,实时显示剩余时间,定时结束自动停机
- 优化电机控制:一档力度提升至75%,降档冲击时间延长至800ms,降档时触发100%冲击防卡顿
- 屏幕显示重构为分区动态更新:时间状态(Zone1)/档位(Zone2)/加热(Zone3),性能优化
- 新增LED指示:运行时RUN_LED以500ms周期闪烁,无效操作ERR_LED亮1秒
- 操作逻辑改进:所有操作需先设定时间,否则播放"请先设定时间"并亮ERR_LED
- 加热控制由GPIO改为PWM(TIM1_CH1),占空比300
- 调整USART3波特率为9600适配MP3模块
- FreeRTOS任务优化:Motor周期500ms,Screen周期10ms,新增定时器秒Tick处理
- 完善开发文档:MP3集成指南、校验和验证脚本、文件重命名工具等

新增文件:mp3_driver.h/c, 多项开发文档
修改文件:freertos.c, gbk_text.h/c, motor_driver.c, screen.h/c, usart.c, gpio.c等
This commit is contained in:
2026-02-17 23:52:17 +08:00
parent 4c37261cc8
commit b883e0a7f9
16 changed files with 1802 additions and 184 deletions

View File

@@ -51,6 +51,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE
Core/Src/motor_driver.c Core/Src/motor_driver.c
Core/Src/screen.c Core/Src/screen.c
Core/Src/gbk_text.c Core/Src/gbk_text.c
Core/Src/mp3_driver.c
) )
# Add include paths # Add include paths

View File

@@ -6,46 +6,46 @@
// GBK文本数组声明定义在gbk_text.c中 // GBK文本数组声明定义在gbk_text.c中
extern const uint8_t text_device_name_GBK[]; extern const uint8_t text_device_name_GBK[];
extern const uint8_t text_loading_GBK[]; extern const uint8_t text_loading_GBK[];
extern const uint8_t text_massage_on_GBK[];
extern const uint8_t text_massage_off_GBK[]; extern const uint8_t text_massage_off_GBK[];
extern const uint8_t text_heat_on_GBK[]; extern const uint8_t text_heat_on_GBK[];
extern const uint8_t text_heat_off_GBK[]; extern const uint8_t text_heat_off_GBK[];
extern const uint8_t text_gear_GBK[]; extern const uint8_t text_gear_1_GBK[];
extern const uint8_t text_time_GBK[]; extern const uint8_t text_gear_2_GBK[];
extern const uint8_t text_cont_GBK[]; extern const uint8_t text_gear_3_GBK[];
extern const uint8_t text_time_1_GBK[];
extern const uint8_t text_time_2_GBK[];
extern const uint8_t text_time_3_GBK[];
extern const uint8_t text_remaining_GBK[]; extern const uint8_t text_remaining_GBK[];
extern const uint8_t text_running_GBK[]; extern const uint8_t text_stop_GBK[];
extern const uint8_t text_paused_GBK[];
extern const uint8_t text_ended_GBK[];
// GBK文本长度宏预计算不含\0 // GBK文本长度宏预计算不含\0
#define text_device_name_LEN (10) #define text_device_name_LEN (10)
#define text_loading_LEN (6) #define text_loading_LEN (6)
#define text_massage_on_LEN (8) #define text_massage_off_LEN (13)
#define text_massage_off_LEN (8) #define text_heat_on_LEN (13)
#define text_heat_on_LEN (8) #define text_heat_off_LEN (13)
#define text_heat_off_LEN (8) #define text_gear_1_LEN (15)
#define text_gear_LEN (4) #define text_gear_2_LEN (15)
#define text_time_LEN (6) #define text_gear_3_LEN (15)
#define text_cont_LEN (4) #define text_time_1_LEN (15)
#define text_remaining_LEN (8) #define text_time_2_LEN (15)
#define text_running_LEN (6) #define text_time_3_LEN (15)
#define text_paused_LEN (6) #define text_remaining_LEN (13)
#define text_ended_LEN (6) #define text_stop_LEN (14)
// 兼容旧代码的别名 // 兼容旧代码的别名
#define text_device_name text_device_name_GBK #define text_device_name text_device_name_GBK
#define text_loading text_loading_GBK #define text_loading text_loading_GBK
#define text_massage_on text_massage_on_GBK
#define text_massage_off text_massage_off_GBK #define text_massage_off text_massage_off_GBK
#define text_heat_on text_heat_on_GBK #define text_heat_on text_heat_on_GBK
#define text_heat_off text_heat_off_GBK #define text_heat_off text_heat_off_GBK
#define text_gear text_gear_GBK #define text_gear_1 text_gear_1_GBK
#define text_time text_time_GBK #define text_gear_2 text_gear_2_GBK
#define text_cont text_cont_GBK #define text_gear_3 text_gear_3_GBK
#define text_time_1 text_time_1_GBK
#define text_time_2 text_time_2_GBK
#define text_time_3 text_time_3_GBK
#define text_remaining text_remaining_GBK #define text_remaining text_remaining_GBK
#define text_running text_running_GBK #define text_stop text_stop_GBK
#define text_paused text_paused_GBK
#define text_ended text_ended_GBK
#endif // __GBK_TEXT_H__ #endif // __GBK_TEXT_H__

45
Core/Inc/mp3_driver.h Normal file
View File

@@ -0,0 +1,45 @@
#ifndef __MP3_DRIVER_H
#define __MP3_DRIVER_H
#ifdef __cplusplus
extern "C" {
#endif
#include "stm32f1xx_hal.h"
#include <stdint.h>
/* MP3命令定义 */
#define MP3_HEADER 0x7E // 帧头
#define MP3_VERSION 0xFF // 版本号
#define MP3_LENGTH 0x06 // 数据长度
#define MP3_FOOTER 0xEF // 帧尾
/* MP3功能码 */
#define MP3_CMD_SET_VOLUME 0x06 // 设置音量
#define MP3_CMD_SET_EQ 0x07 // 设置EQ
#define MP3_CMD_SET_MODE 0x08 // 设置播放模式
#define MP3_CMD_SET_SOURCE 0x09 // 设置音源
#define MP3_CMD_PLAY 0x0D // 播放
#define MP3_CMD_PAUSE 0x0E // 暂停
#define MP3_CMD_PREV 0x0A // 上一曲
#define MP3_CMD_NEXT 0x0B // 下一曲
#define MP3_CMD_PLAY_INDEX 0x12 // 按索引播放
#define MP3_CMD_STOP 0x16 // 停止
/* MP3音源选择 */
#define MP3_SOURCE_U_DISK 0x01 // U盘
#define MP3_SOURCE_SD_CARD 0x02 // TF卡/SD卡
/* 函数声明 */
HAL_StatusTypeDef MP3_Init(void);
HAL_StatusTypeDef MP3_Play(uint16_t index);
HAL_StatusTypeDef MP3_Stop(void);
HAL_StatusTypeDef MP3_Pause(void);
HAL_StatusTypeDef MP3_SetVolume(uint8_t volume);
HAL_StatusTypeDef MP3_SetSource(uint8_t source);
#ifdef __cplusplus
}
#endif
#endif /* __MP3_DRIVER_H */

View File

@@ -23,7 +23,6 @@
#include "main.h" #include "main.h"
#include "task.h" #include "task.h"
/* Private includes ----------------------------------------------------------*/ /* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */ /* USER CODE BEGIN Includes */
#include "elog.h" #include "elog.h"
@@ -36,7 +35,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <sys/_intsup.h> #include <sys/_intsup.h>
#include "mp3_driver.h"
/* USER CODE END Includes */ /* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/ /* Private typedef -----------------------------------------------------------*/
@@ -64,7 +63,7 @@ PUTCHAR_PROTOTYPE {
/* USER CODE END PM */ /* USER CODE END PM */
/* Private variables ---------------------------------------------------------*/ /* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */ /* USER CODE BEGIN Variables */
/* 按键状态位: bit0..bit3 分别对应 M__KEY, M__KEYC7, HOT_KEY, TIME_KEY, /* 按键状态位: bit0..bit3 分别对应 M__KEY, M__KEYC7, HOT_KEY, TIME_KEY,
* 为1表示按下 */ * 为1表示按下 */
@@ -72,8 +71,14 @@ volatile uint8_t key_state = 0;
volatile uint8_t key_state_prev = 0; /* 上一次的按键状态,用于检测按键变化 */ volatile uint8_t key_state_prev = 0; /* 上一次的按键状态,用于检测按键变化 */
/* 设备状态标志0表示关闭1表示打开 */ /* 设备状态标志0表示关闭1表示打开 */
static uint8_t hot_state = 0; /* HOT 设备状态 */ static uint8_t hot_state = 0; /* HOT 设备状态 */
static uint8_t time_state = 0; /* TIME_KEY 时间状态 */
/* 定时器相关变量 */
static uint8_t timer_minutes = 0; // 当前设定的分钟数 (0/10/20/30)
static uint32_t remaining_seconds = 0; // 剩余秒数
static uint8_t is_running = 0; // 运行状态标志 (0=停止, 1=运行)
static uint8_t last_display_minutes = 0xFF; // 上一次显示的分钟数
static uint8_t run_led_state = 0; // RUN_LED闪烁状态 (0=灭, 1=亮)
/* USER CODE END Variables */ /* USER CODE END Variables */
/* Definitions for defaultTask */ /* Definitions for defaultTask */
@@ -125,6 +130,127 @@ const osEventFlagsAttr_t init_ok_attributes = {.name = "init_ok"};
/* Private function prototypes -----------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/
/* USER CODE BEGIN FunctionPrototypes */ /* USER CODE BEGIN FunctionPrototypes */
/**
* @brief 检查是否可以进行操作(电机/加热)
* @return 1=允许操作, 0=禁止操作
*/
uint8_t Timer_CanOperate(void) { return (timer_minutes > 0) ? 1 : 0; }
/**
* @brief 获取剩余秒数(供外部调用显示倒计时)
* @return 剩余秒数
*/
uint32_t Timer_GetRemainingSeconds(void) { return remaining_seconds; }
/**
* @brief 获取设定的分钟数
* @return 设定的分钟数
*/
uint8_t Timer_GetMinutes(void) { return timer_minutes; }
/**
* @brief 获取运行状态
* @return 1=运行中, 0=停止
*/
uint8_t Timer_IsRunning(void) { return is_running; }
/**
* @brief 动态生成剩余时间 GBK 字符串
* @param minutes 剩余分钟数 (0-99)
* @param buf 输出缓冲区(至少需要 14 字节)
* @return 生成的字符串长度
*/
static uint8_t FormatRemainingTime(uint8_t minutes, uint8_t *buf) {
uint8_t len = 0;
// 固定前缀 "时间:剩余" (GBK: 0xCA,0xB1,0xBC,0xE4,0x3A,0xCA,0xA3,0xD3,0xE0)
const uint8_t prefix[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A,
0xCA, 0xA3, 0xD3, 0xE0};
for (uint8_t i = 0; i < sizeof(prefix); i++) {
buf[len++] = prefix[i];
}
// 动态数字部分:将分钟数转换为 ASCII
if (minutes >= 10) {
buf[len++] = '0' + (minutes / 10); // 十位
}
buf[len++] = '0' + (minutes % 10); // 个位
// 固定后缀 "分" (GBK: 0xB7, 0xD6)
buf[len++] = 0xB7;
buf[len++] = 0xD6;
return len;
}
// 全局缓冲区
static uint8_t gbk_remaining_buf[16];
/**
* @brief 更新 Zone 1 的时间显示
*/
static void Display_UpdateTime(void) {
if (is_running) {
// 运行中:显示剩余时间(动态计算)
uint8_t remaining_minutes = remaining_seconds / 60;
uint8_t len = FormatRemainingTime(remaining_minutes, gbk_remaining_buf);
Screen_ShowInZone(1, gbk_remaining_buf, len);
} else if (timer_minutes > 0) {
// 已设定时间但未运行:显示设定时间
switch (timer_minutes) {
case 10:
Screen_ShowInZone(1, text_time_1, text_time_1_LEN);
break;
case 20:
Screen_ShowInZone(1, text_time_2, text_time_2_LEN);
break;
case 30:
Screen_ShowInZone(1, text_time_3, text_time_3_LEN);
break;
default:
break;
}
} else {
// 无定时:显示停止状态
Screen_ShowInZone(1, text_stop, text_stop_LEN);
}
}
/**
* @brief 定时器倒计时处理(每秒调用一次)
*/
void Timer_Tick(void) {
if (is_running && remaining_seconds > 0) {
remaining_seconds--;
// 时间到,停止所有设备
if (remaining_seconds == 0) {
is_running = 0;
timer_minutes = 0;
// 停止电机
Motor_SetGear(0);
// 停止加热
hot_state = 0;
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);
elog_i("Timer", "定时结束,设备已停止");
// 播放语音: 定时结束,按摩结束
MP3_Play(18);
// 更新 Zone 1 显示为停止状态
Screen_ShowInZone(1, text_stop, text_stop_LEN);
// 更新 Zone 2 显示为停止状态(按摩已停止)
Screen_ShowInZone(2, text_massage_off, text_massage_off_LEN);
// 重置显示记录,避免下次启动时的显示问题
last_display_minutes = 0xFF;
}
}
}
/* USER CODE END FunctionPrototypes */ /* USER CODE END FunctionPrototypes */
void StartDefaultTask(void *argument); void StartDefaultTask(void *argument);
@@ -228,10 +354,11 @@ void StartDefaultTask(void *argument) {
if (flags == ((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4))) { if (flags == ((1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4))) {
// 所有初始化完成,开始执行按摩逻辑 // 所有初始化完成,开始执行按摩逻辑
// Screen_DrawText16V_GBK(1, 30, text_stop_massage, text_stop_massage_LEN, 15); // Screen_DrawText16V_GBK(1, 30, text_stop_massage, text_stop_massage_LEN,
Screen_ShowInZone(1, text_ended, text_ended_LEN); // 15);
Screen_ShowInZone(1, text_stop, text_stop_LEN);
Screen_ShowInZone(2, text_massage_off, text_massage_off_LEN); Screen_ShowInZone(2, text_massage_off, text_massage_off_LEN);
Screen_ShowInZone(3, text_heat_off,text_heat_off_LEN); Screen_ShowInZone(3, text_heat_off, text_heat_off_LEN);
} }
elog_d("Init", "完成所有的初始化"); elog_d("Init", "完成所有的初始化");
@@ -315,43 +442,158 @@ void Sensor(void *argument) {
/* ===== M__KEY (bit0) 控制 ===== */ /* ===== M__KEY (bit0) 控制 ===== */
if (key_pressed & (1 << 0)) { if (key_pressed & (1 << 0)) {
/* M__KEY 按下:加档(提高转速) */ if (Timer_CanOperate()) { // 添加判断
elog_i("Key", "M__KEY按下 - 加档"); /* M__KEY 按下:加档(提高转速) */
if (Motor_GetGear() == 0) { elog_i("Key", "M__KEY按下 - 加档");
Motor_StartupBoost(); if (Motor_GetGear() == 0) {
Motor_StartupBoost();
is_running = 1; // 开始运行
remaining_seconds = timer_minutes * 60;
// 播放语音: 按摩开始
MP3_Play(7);
} else {
// 检查当前档位如果已经是3档则提示已到最大
uint8_t old_gear = Motor_GetGear();
if (old_gear == 3) {
MP3_Play(11); // 已到最大档位
} else {
Motor_GearUp();
uint8_t new_gear = Motor_GetGear();
// 档位变化时播放语音
if (new_gear == 1) {
MP3_Play(8); // 一档
} else if (new_gear == 2) {
MP3_Play(9); // 二档
} else if (new_gear == 3) {
MP3_Play(10); // 三档
}
}
}
} else { } else {
Motor_GearUp(); elog_w("Key", "M__KEY无效 - 请先设定时间");
// 播放语音: 请先设定时间
MP3_Play(17);
elog_d("LED", "Sensor任务: ERR_LED亮");
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_RESET); // ERR亮
osDelay(1000); // LED亮1秒
elog_d("LED", "Sensor任务: ERR_LED灭");
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_SET); // ERR灭
} }
} }
/* ===== M__KEYC7 (bit1) 控制 ===== */ /* ===== M__KEYC7 (bit1) 控制 ===== */
if (key_pressed & (1 << 1)) { if (key_pressed & (1 << 1)) {
/* M__KEYC7 按下:降档(降低转速) */ if (Timer_CanOperate()) { // 添加判断
elog_i("Key", "M__KEYC7按下 - 降档"); /* M__KEYC7 按下:降档(降低转速) */
Motor_GearDown(); elog_i("Key", "M__KEYC7按下 - 降档");
Motor_GearDown();
uint8_t new_gear = Motor_GetGear();
// 降档时播放语音
if (new_gear == 0) {
MP3_Play(14); // 按摩停止
is_running = 0;
} else if (new_gear == 1) {
MP3_Play(12); // 一档
} else if (new_gear == 2) {
MP3_Play(13); // 二档
}
} else {
elog_w("Key", "M__KEYC7无效 - 请先设定时间");
// 播放语音: 请先设定时间
MP3_Play(17);
elog_d("LED", "Sensor任务: ERR_LED亮");
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_RESET); // ERR亮
osDelay(1000); // LED亮1秒
elog_d("LED", "Sensor任务: ERR_LED灭");
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_SET); // ERR灭
}
} }
/* ===== HOT_KEY (bit2) 控制 ===== */ /* ===== HOT_KEY (bit2) 控制 ===== */
if (key_pressed & (1 << 2)) { if (key_pressed & (1 << 2)) {
/* HOT_KEY 按下:切换热功能 */ if (Timer_CanOperate()) { // 添加判断
hot_state = !hot_state; /* HOT_KEY 按下:切换热功能 */
if (hot_state) { hot_state = !hot_state;
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 300); if (hot_state) {
elog_d("Hot", "设置PWM为300"); __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 300);
elog_d("Hot", "设置PWM为300");
// 播放语音: 加热已开启
MP3_Play(15);
} else {
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);
elog_d("Hot", "设置PWM为0");
// 播放语音: 加热已关闭
MP3_Play(16);
}
} else { } else {
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); /* 关闭加热 */ elog_w("Key", "HOT_KEY无效 - 请先设定时间");
elog_d("Hot", "设置PWM为0"); // 播放语音: 请先设定时间
MP3_Play(17);
elog_d("LED", "Sensor任务: ERR_LED亮");
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_RESET); // ERR亮
osDelay(1000); // LED亮1秒
elog_d("LED", "Sensor任务: ERR_LED灭");
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_SET); // ERR灭
} }
} }
/* ===== TIME_KEY (bit3) 控制 ===== */ /* ===== TIME_KEY (bit3) 控制 ===== */
if (key_pressed & (1 << 3)) { if (key_pressed & (1 << 3)) {
/* TIME_KEY 按下:切换定时 */ /* TIME_KEY 按下:循环设定时间 0→10→20→30→0 */
time_state = !time_state; if (!is_running) {
if (time_state) { if (timer_minutes >= 30) {
// TODO: 添加定时启动逻辑 timer_minutes = 0;
} else {
timer_minutes += 10;
}
elog_i("Timer", "设定时间: %d 分钟", timer_minutes);
// 播放语音提示
if (timer_minutes == 10) {
MP3_Play(2); // 定时10分钟
} else if (timer_minutes == 20) {
MP3_Play(3); // 定时20分钟
} else if (timer_minutes == 30) {
MP3_Play(4); // 定时30分钟
} else if (timer_minutes == 0) {
MP3_Play(5); // 定时已取消
}
// 如果设定了时间,显示在屏幕上(调用你的倒计时接口)
if (timer_minutes > 0) {
remaining_seconds = timer_minutes * 60;
Display_UpdateTime();
} else {
Screen_ShowInZone(1, text_stop, text_stop_LEN);
}
} else { } else {
// TODO: 添加定时关闭逻辑 // 运行中按下TIME_KEY取消定时并停止
is_running = 0;
timer_minutes = 0;
remaining_seconds = 0;
// 停止电机和加热
Motor_SetGear(0);
hot_state = 0;
__HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0);
elog_i("Timer", "定时取消,设备已停止");
// 播放语音: 按摩已停止
MP3_Play(6);
// 更新 Zone 1 显示为停止状态
Screen_ShowInZone(1, text_stop, text_stop_LEN);
// 延时一下,确保屏幕更新完成
osDelay(50);
// 更新 Zone 2 显示为停止状态(按摩已停止)
Screen_ShowInZone(2, text_massage_off, text_massage_off_LEN);
// 重置显示记录
last_display_minutes = 0xFF;
} }
} }
@@ -374,27 +616,65 @@ void Motor(void *argument) {
/* 电机驱动初始化 */ /* 电机驱动初始化 */
Motor_Init(); Motor_Init();
/* 初始化LED状态 (LED是低电平点亮GPIO_PIN_SET灭GPIO_PIN_RESET亮) */
HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, GPIO_PIN_SET); // RUN灭
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_SET); // ERR灭
run_led_state = 0;
elog_d("LED", "Motor任务初始化: RUN_LED灭, ERR_LED灭");
osEventFlagsSet(init_okHandle, 1 << 2); osEventFlagsSet(init_okHandle, 1 << 2);
/* Infinite loop */ /* Infinite loop */
static uint32_t tick_counter = 0;
for (;;) { for (;;) {
/* 电机任务主要功能: tick_counter++;
* - 监控电机状态(超温保护、过流保护等)
* - 定期打印状态信息
* - 响应紧急停止信号
*/
/* 每1秒打印一次电机状态 */ // 每1000ms调用一次Timer_Tick每2个循环周期
uint8_t current_gear = Motor_GetGear(); if (tick_counter % 2 == 0) {
elog_i("Motor", "当前档位: %u (0=停止,1=低,2=中,3=高)", current_gear); Timer_Tick(); // 倒计时处理
}
HAL_GPIO_TogglePin(RUN_LED_GPIO_Port, RUN_LED_Pin); // RUN_LED闪烁控制运行时500ms翻转一次 (LED低电平点亮)
osDelay(1000); if (is_running) {
run_led_state = !run_led_state;
HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, run_led_state ? GPIO_PIN_RESET : GPIO_PIN_SET);
} else {
// 停止时保持灭
if (run_led_state != 0) {
run_led_state = 0;
HAL_GPIO_WritePin(RUN_LED_GPIO_Port, RUN_LED_Pin, GPIO_PIN_SET);
}
}
// ERR_LED确保熄灭LED低电平点亮所以灭用GPIO_PIN_SET
static uint8_t last_err_led_state = 0;
HAL_GPIO_WritePin(ERR_LED_GPIO_Port, ERR_LED_Pin, GPIO_PIN_SET);
if (last_err_led_state != 0) {
elog_d("LED", "Motor任务: ERR_LED灭");
last_err_led_state = 0;
}
// 每1000ms更新显示每2个循环周期检查一次
if (tick_counter % 2 == 0) {
if (is_running) {
uint8_t current_minutes = remaining_seconds / 60;
// 只有当分钟数变化时才刷新屏幕
if (current_minutes != last_display_minutes) {
last_display_minutes = current_minutes;
Display_UpdateTime();
elog_d("Screen", "更新倒计时显示: %d分", current_minutes);
}
}
}
osDelay(500); // 500ms循环周期
} }
/* USER CODE END Motor */ /* USER CODE END Motor */
} }
/* USER CODE BEGIN Header_Screen */ /* USER CODE BEGIN Header_Screen */
/** /**
* @brief Function implementing the Screen_Tsak thread. * @brief Function implementing the Screen_Tsak thread.
* @param argument: Not used * @param argument: Not used
@@ -403,6 +683,9 @@ void Motor(void *argument) {
/* USER CODE END Header_Screen */ /* USER CODE END Header_Screen */
void Screen(void *argument) { void Screen(void *argument) {
/* USER CODE BEGIN Screen */ /* USER CODE BEGIN Screen */
static uint8_t last_hot_state = 0; // 记录上一次的热状态
static uint8_t last_gear = 0xFF; // 记录上一次的挡位(初始为无效值)
elog_d("Init", "Screen task started"); elog_d("Init", "Screen task started");
osEventFlagsSet(init_okHandle, 1 << 3); osEventFlagsSet(init_okHandle, 1 << 3);
@@ -410,8 +693,50 @@ void Screen(void *argument) {
// HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), HAL_MAX_DELAY); // HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), HAL_MAX_DELAY);
/* Infinite loop */ /* Infinite loop */
for (;;) { for (;;) {
if (hot_state) {
uint8_t current_gear = Motor_GetGear();
// 挡位变化时更新屏幕第2区域显示挡位
if (current_gear != last_gear) {
last_gear = current_gear;
// 0 档位显示停止
if (current_gear == 0) {
Screen_ShowInZone(2, text_massage_off, text_massage_off_LEN);
}
// 1级
if (current_gear == 1) {
Screen_ShowInZone(2, text_gear_1, text_gear_1_LEN);
}
// 2级
else if (current_gear == 2) {
Screen_ShowInZone(2, text_gear_2, text_gear_2_LEN);
}
// 3级
else if (current_gear == 3) {
Screen_ShowInZone(2, text_gear_3, text_gear_3_LEN);
}
} }
// 只在状态变化时更新屏幕
if (hot_state != last_hot_state) {
last_hot_state = hot_state;
if (hot_state) {
// 第3区域显示热功能开启
Screen_ShowInZone(3, text_heat_on, text_heat_on_LEN);
} else {
// 第3区域显示热功能关闭
Screen_ShowInZone(3, text_heat_off, text_heat_off_LEN);
}
}
osDelay(10); // 添加延时避免CPU空转
} }
/* USER CODE END Screen */ /* USER CODE END Screen */
} }
@@ -424,32 +749,54 @@ void Screen(void *argument) {
* @retval None * @retval None
*/ */
/* USER CODE END Header_MP3 */ /* USER CODE END Header_MP3 */
void MP3(void *argument) { void MP3(void *argument) {
/* USER CODE BEGIN MP3 */ /* USER CODE BEGIN MP3 */
elog_d("Init", "MP3 task started"); elog_d("Init", "MP3 task started");
uint8_t init_cmd1[] = {0x7E, 0xFF, 0x06, 0x06, 0x00, elog_d("MP3", "USART3波特率: 9600");
0x00, 0x1E, 0xFE, 0xD7, 0xEF}; // 开启声音
uint8_t init_cmd2[] = {0x7E, 0xFF, 0x06, 0x09, 0x00,
0x00, 0x02, 0xFE, 0xF0, 0xEF}; // TF卡
uint8_t play_cmd[] = {0x7E, 0xFF, 0x06, 0x12, 0x00,
0x00, 0x01, 0xFE, 0xE8, 0xEF}; // 播放文件1
// 等待模块上电稳定 // uint8_t init_cmd1[] = {0x7E, 0xFF, 0x06, 0x06, 0x00,
osDelay(2000); // 0x00, 0x1E, 0xFE, 0xD7, 0xEF}; // 开启声音
// uint8_t init_cmd2[] = {0x7E, 0xFF, 0x06, 0x09, 0x00,
// 0x00, 0x02, 0xFE, 0xF0, 0xEF}; // TF卡
// uint8_t play_cmd[] = {0x7E, 0xFF, 0x06, 0x12, 0x00,
// 0x00, 0x01, 0xFE, 0xE8, 0xEF}; // 播放文件1
// 发送初始化命令1等待模块 ACK // // 等待模块上电稳定
HAL_UART_Transmit(&huart3, init_cmd1, sizeof(init_cmd1), 100); // elog_d("MP3", "等待模块上电稳定...");
// osDelay(2000);
// // 发送初始化命令1等待模块 ACK
// elog_d("MP3", "发送初始化命令1: 开启声音");
// HAL_StatusTypeDef ret1 = HAL_UART_Transmit(&huart3, init_cmd1, sizeof(init_cmd1), 100);
// elog_d("MP3", "命令1返回值: %d", ret1);
// osDelay(100);
// // 发送初始化命令2
// elog_d("MP3", "发送初始化命令2: 选择TF卡");
// HAL_StatusTypeDef ret2 = HAL_UART_Transmit(&huart3, init_cmd2, sizeof(init_cmd2), 100);
// elog_d("MP3", "命令2返回值: %d", ret2);
// osDelay(500);
// 等待模块上电稳定
osDelay(2000);
// 初始化MP3模块
HAL_StatusTypeDef ret = MP3_Init();
if (ret != HAL_OK) {
elog_e("MP3", "MP3模块初始化失败");
return;
}
// 发送初始化命令2
HAL_UART_Transmit(&huart3, init_cmd2, sizeof(init_cmd2), 100);
elog_i("MP3", "模块初始化完成"); elog_i("MP3", "模块初始化完成");
MP3_Play(1);
osEventFlagsSet(init_okHandle, 1 << 4); osEventFlagsSet(init_okHandle, 1 << 4);
/* Infinite loop */ /* Infinite loop */
for (;;) { for (;;) {
// 发送播放命令
HAL_UART_Transmit(&huart3, play_cmd, sizeof(play_cmd), 100);
osDelay(10000); osDelay(10000);
} }

View File

@@ -8,47 +8,47 @@ const uint8_t text_device_name_GBK[] = {0xD6, 0xC7, 0xC4, 0xDC, 0xB0, 0xB4, 0xC4
// 对应GBK编码0xBC, 0xD3, 0xD4, 0xD8, 0xD6, 0xD0 // 对应GBK编码0xBC, 0xD3, 0xD4, 0xD8, 0xD6, 0xD0
const uint8_t text_loading_GBK[] = {0xBC, 0xD3, 0xD4, 0xD8, 0xD6, 0xD0}; const uint8_t text_loading_GBK[] = {0xBC, 0xD3, 0xD4, 0xD8, 0xD6, 0xD0};
// 对应文本UTF8按摩启动 // 对应文本UTF8按摩:设备停止
// 对应GBK编码0xB0, 0xB4, 0xC4, 0xA6, 0xC6, 0xF4, 0xB6, 0xAF // 对应GBK编码0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xC9, 0xE8, 0xB1, 0xB8, 0xCD, 0xA3, 0xD6, 0xB9
const uint8_t text_massage_on_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0xC6, 0xF4, 0xB6, 0xAF}; const uint8_t text_massage_off_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xC9, 0xE8, 0xB1, 0xB8, 0xCD, 0xA3, 0xD6, 0xB9};
// 对应文本UTF8按摩停止 // 对应文本UTF8加热:开始加热
// 对应GBK编码0xB0, 0xB4, 0xC4, 0xA6, 0xCD, 0xA3, 0xD6, 0xB9 // 对应GBK编码0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xBF, 0xAA, 0xCA, 0xBC, 0xBC, 0xD3, 0xC8, 0xC8
const uint8_t text_massage_off_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0xCD, 0xA3, 0xD6, 0xB9}; const uint8_t text_heat_on_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xBF, 0xAA, 0xCA, 0xBC, 0xBC, 0xD3, 0xC8, 0xC8};
// 对应文本UTF8加热启动 // 对应文本UTF8加热:停止加热
// 对应GBK编码0xBC, 0xD3, 0xC8, 0xC8, 0xC6, 0xF4, 0xB6, 0xAF // 对应GBK编码0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xCD, 0xA3, 0xD6, 0xB9, 0xBC, 0xD3, 0xC8, 0xC8
const uint8_t text_heat_on_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0xC6, 0xF4, 0xB6, 0xAF}; const uint8_t text_heat_off_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xCD, 0xA3, 0xD6, 0xB9, 0xBC, 0xD3, 0xC8, 0xC8};
// 对应文本UTF8加热停止 // 对应文本UTF8按摩:当前低档位
// 对应GBK编码0xBC, 0xD3, 0xC8, 0xC8, 0xCD, 0xA3, 0xD6, 0xB9 // 对应GBK编码0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB5, 0xCD, 0xB5, 0xB5, 0xCE, 0xBB
const uint8_t text_heat_off_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0xCD, 0xA3, 0xD6, 0xB9}; const uint8_t text_gear_1_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB5, 0xCD, 0xB5, 0xB5, 0xCE, 0xBB};
// 对应文本UTF8%d级 // 对应文本UTF8按摩:当前中档位
// 对应GBK编码0x25, 0x64, 0xBC, 0xB6 // 对应GBK编码0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xD6, 0xD0, 0xB5, 0xB5, 0xCE, 0xBB
const uint8_t text_gear_GBK[] = {0x25, 0x64, 0xBC, 0xB6}; const uint8_t text_gear_2_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xD6, 0xD0, 0xB5, 0xB5, 0xCE, 0xBB};
// 对应文本UTF8%d分钟 // 对应文本UTF8按摩:当前高档位
// 对应GBK编码0x25, 0x64, 0xB7, 0xD6, 0xD6, 0xD3 // 对应GBK编码0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB8, 0xDF, 0xB5, 0xB5, 0xCE, 0xBB
const uint8_t text_time_GBK[] = {0x25, 0x64, 0xB7, 0xD6, 0xD6, 0xD3}; const uint8_t text_gear_3_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB8, 0xDF, 0xB5, 0xB5, 0xCE, 0xBB};
// 对应文本UTF8持续 // 对应文本UTF8时间:设定10分钟
// 对应GBK编码0xB3, 0xD6, 0xD0, 0xF8 // 对应GBK编码0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x31, 0x30, 0xB7, 0xD6, 0xD6, 0xD3
const uint8_t text_cont_GBK[] = {0xB3, 0xD6, 0xD0, 0xF8}; const uint8_t text_time_1_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x31, 0x30, 0xB7, 0xD6, 0xD6, 0xD3};
// 对应文本UTF8剩余%d分 // 对应文本UTF8时间:设定20分钟
// 对应GBK编码0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6 // 对应GBK编码0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x32, 0x30, 0xB7, 0xD6, 0xD6, 0xD3
const uint8_t text_remaining_GBK[] = {0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6}; const uint8_t text_time_2_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x32, 0x30, 0xB7, 0xD6, 0xD6, 0xD3};
// 对应文本UTF8运行中 // 对应文本UTF8时间:设定30分钟
// 对应GBK编码0xD4, 0xCB, 0xD0, 0xD0, 0xD6, 0xD0 // 对应GBK编码0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x33, 0x30, 0xB7, 0xD6, 0xD6, 0xD3
const uint8_t text_running_GBK[] = {0xD4, 0xCB, 0xD0, 0xD0, 0xD6, 0xD0}; const uint8_t text_time_3_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x33, 0x30, 0xB7, 0xD6, 0xD6, 0xD3};
// 对应文本UTF8已暂停 // 对应文本UTF8时间:剩余%d分
// 对应GBK编码0xD2, 0xD1, 0xD4, 0xDD, 0xCD, 0xA3 // 对应GBK编码0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6
const uint8_t text_paused_GBK[] = {0xD2, 0xD1, 0xD4, 0xDD, 0xCD, 0xA3}; const uint8_t text_remaining_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6};
// 对应文本UTF8已结束 // 对应文本UTF8机器运行已停止
// 对应GBK编码0xD2, 0xD1, 0xBD, 0xE1, 0xCA, 0xF8 // 对应GBK编码0xBB, 0xFA, 0xC6, 0xF7, 0xD4, 0xCB, 0xD0, 0xD0, 0xD2, 0xD1, 0xCD, 0xA3, 0xD6, 0xB9
const uint8_t text_ended_GBK[] = {0xD2, 0xD1, 0xBD, 0xE1, 0xCA, 0xF8}; const uint8_t text_stop_GBK[] = {0xBB, 0xFA, 0xC6, 0xF7, 0xD4, 0xCB, 0xD0, 0xD0, 0xD2, 0xD1, 0xCD, 0xA3, 0xD6, 0xB9};

View File

@@ -20,13 +20,15 @@
/* Private function prototypes -----------------------------------------------*/ /* Private function prototypes -----------------------------------------------*/
void Motor_StartAll(void);
void Motor_StopAll(void);
/* PWM parameters */ /* PWM parameters */
#define PWM_ARR 99 #define PWM_ARR 99
#define PWM_STEPS (PWM_ARR + 1) #define PWM_STEPS (PWM_ARR + 1)
/* 三档占空比(百分比),索引为 0..2 对应 档位 1..3档位0为停止 */ /* 三档占空比(百分比),索引为 0..2 对应 档位 1..3档位0为停止 */
static const uint8_t gears_percent[3] = {55, 65, 75}; static const uint8_t gears_percent[3] = {75, 85, 95};
/* current_gear: 0 = 停止, 1 = 低, 2 = 中, 3 = 高 */ /* current_gear: 0 = 停止, 1 = 低, 2 = 中, 3 = 高 */
static uint8_t current_gear = 0; /* 0..3 */ static uint8_t current_gear = 0; /* 0..3 */
@@ -34,25 +36,32 @@ static inline uint32_t percent_to_compare(uint8_t percent) {
return ((uint32_t)percent * PWM_STEPS) / 100U; return ((uint32_t)percent * PWM_STEPS) / 100U;
} }
/* 将两个电机都设置为指定占空比(使用 TIM4_CH2, TIM4_CH4 输出CH1/CH3 停止以保持单方向 */ /* 将两个电机都设置为指定占空比(使用 AT8236-MS H桥驱动 */
static void set_both_motors_percent(uint8_t percent) { static void set_both_motors_percent(uint8_t percent) {
uint32_t cmp = percent_to_compare(percent); uint32_t cmp = percent_to_compare(percent);
/* 停止并关闭 CH1、使用 CH2 输出 */ /* 电机1: TIM4_CH1->AIN1, TIM4_CH2->AIN2
//HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1); * 正转AIN1=PWM, AIN2=0
* 反转AIN1=0, AIN2=PWM
* 这里用正转模式CH1输出PWM, CH2为0 */
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, (uint16_t)cmp); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, (uint16_t)cmp);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 100); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 0);
/* 停止并关闭 CH3、使用 CH4 输出 */ /* 电机2: TIM4_CH3->BIN1, TIM4_CH4->BIN2
//HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_3); * 正转BIN1=PWM, BIN2=0
* 这里用正转模式CH3输出PWM, CH4为0 */
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, (uint16_t)cmp); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, (uint16_t)cmp);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 100); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);
} }
/* 启动时短暂提升到 75% 并保持 500ms然后降到一档档位1 */ /* 启动时短暂提升到 100% 并保持 500ms然后降到一档档位1 */
void Motor_StartupBoost(void) { void Motor_StartupBoost(void) {
set_both_motors_percent(80); /* 启动提升到 80% */ // 确保PWM通道已启动
Motor_StartAll();
set_both_motors_percent(100); /* 启动提升到 100% */
elog_d("Motor", "启动冲击: 100%%");
osDelay(1500); osDelay(1500);
current_gear = 1; /* 启动后回到档位1最低速 */ current_gear = 1; /* 启动后回到档位1最低速 */
elog_d("Motor", "设置档位: %d, PWM: %d%%", current_gear, gears_percent[current_gear - 1]);
set_both_motors_percent(gears_percent[current_gear - 1]); set_both_motors_percent(gears_percent[current_gear - 1]);
} }
@@ -61,9 +70,13 @@ void Motor_SetGear(uint8_t gear) {
if (gear > 3) gear = 3; if (gear > 3) gear = 3;
current_gear = gear; current_gear = gear;
if (current_gear == 0) { if (current_gear == 0) {
// set_both_motors_percent(0); // 完全停止所有PWM输出
Motor_Stop(); Motor_Stop();
elog_d("Motor", "Motor_SetGear: 档位0, 停止");
} else { } else {
// 确保PWM通道已启动
Motor_StartAll();
elog_d("Motor", "Motor_SetGear: 档位%d, PWM: %d%%", current_gear, gears_percent[current_gear - 1]);
set_both_motors_percent(gears_percent[current_gear - 1]); set_both_motors_percent(gears_percent[current_gear - 1]);
} }
} }
@@ -72,10 +85,15 @@ void Motor_SetGear(uint8_t gear) {
void Motor_NextGear(void) { void Motor_NextGear(void) {
current_gear++; current_gear++;
if (current_gear > 3) current_gear = 0; if (current_gear > 3) current_gear = 0;
if (current_gear == 0) { if (current_gear == 0) {
// set_both_motors_percent(0); // 完全停止所有PWM输出
Motor_Stop(); Motor_Stop();
elog_d("Motor", "Motor_NextGear: 档位0, 停止");
} else { } else {
// 确保PWM通道已启动
Motor_StartAll();
elog_d("Motor", "Motor_NextGear: 档位%d, PWM: %d%%", current_gear, gears_percent[current_gear - 1]);
set_both_motors_percent(gears_percent[current_gear - 1]); set_both_motors_percent(gears_percent[current_gear - 1]);
} }
} }
@@ -87,14 +105,33 @@ void Motor_GearUp(void) {
/* 向下减档(循环) */ /* 向下减档(循环) */
void Motor_GearDown(void) { void Motor_GearDown(void) {
uint8_t old_gear = current_gear;
if (current_gear == 0) { if (current_gear == 0) {
current_gear = 3; current_gear = 3;
} else { } else {
current_gear--; current_gear--;
} }
elog_d("Motor", "Motor_GearDown: %d -> %d", old_gear, current_gear);
if (current_gear == 0) { if (current_gear == 0) {
set_both_motors_percent(0); // 完全停止所有PWM输出
Motor_Stop();
elog_d("Motor", "档位0, 停止");
} else { } else {
// 确保PWM通道已启动
Motor_StartAll();
// 如果从2档或3档降到1档需要短暂冲击保持电机转动
if (current_gear == 1 && old_gear >= 2) {
// 短暂提升到100%来维持转动然后降到1档
elog_d("Motor", "降档冲击: 100%%");
set_both_motors_percent(100);
osDelay(800); // 800ms冲击给电机更充裕的适应时间
}
elog_d("Motor", "最终档位%d, PWM: %d%%", current_gear, gears_percent[current_gear - 1]);
set_both_motors_percent(gears_percent[current_gear - 1]); set_both_motors_percent(gears_percent[current_gear - 1]);
} }
} }
@@ -122,10 +159,44 @@ void Motor_Init(void) {
} }
/**
* @brief 完全停止电机并关闭所有PWM通道
*/
void Motor_StopAll(void) {
// 设置所有通道的PWM为0
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 0);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, 0);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);
// 停止所有PWM通道
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_2);
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_3);
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_4);
}
/**
* @brief 重新启动所有PWM通道用于启动电机
*/
void Motor_StartAll(void) {
// 重新启动PWM通道
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4);
}
void Motor_Stop(void) { void Motor_Stop(void) {
// 所有引脚输出0 - 完全停止 // 所有引脚输出0 - 完全停止
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 0);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 0); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 0);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, 0); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, 0);
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0); __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0);
// 强制停止PWM通道确保电机完全停止
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_2);
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_3);
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_4);
} }

265
Core/Src/mp3_driver.c Normal file
View File

@@ -0,0 +1,265 @@
#include "mp3_driver.h"
#include "elog.h"
#include <string.h>
/* 外部UART句柄声明 */
extern UART_HandleTypeDef huart3;
/**
* @brief 计算MP3命令校验和
* @param cmd 命令数组(不包含校验位)
* @param len 数据长度
* @return 校验和(高位在前,低位在后)
* @note 校验算法从版本号开始到数据结束所有字节相加后取反再加1
*/
static void MP3_CalcChecksum(const uint8_t *cmd, uint8_t len, uint8_t *high, uint8_t *low)
{
uint16_t sum = 0;
// 从版本号(索引1)开始累加,到数据结束
for (uint8_t i = 1; i < len; i++) {
sum += cmd[i];
}
// 取反加1 (即0x10000 - sum)
sum = 0x10000 - sum;
// 高位在前,低位在后
*high = (sum >> 8) & 0xFF;
*low = sum & 0xFF;
}
/**
* @brief 发送MP3命令
* @param cmd 命令数组
* @param len 命令长度
* @return HAL状态
*/
static HAL_StatusTypeDef MP3_SendCommand(const uint8_t *cmd, uint16_t len)
{
return HAL_UART_Transmit(&huart3, (uint8_t *)cmd, len, 100);
}
/**
* @brief MP3模块初始化
* @return HAL状态
*/
HAL_StatusTypeDef MP3_Init(void)
{
elog_d("MP3", "开始初始化MP3模块...");
// 命令1开启声音 (0x06 - 0x00 - 0x00 - 0x1E)
// 0x1E = 30, 表示音量为30
uint8_t init_cmd1[] = {0x7E, 0xFF, 0x06, 0x06, 0x00, 0x00, 0x1E, 0xFE, 0xD7, 0xEF};
uint8_t high1, low1;
MP3_CalcChecksum(init_cmd1, 7, &high1, &low1);
init_cmd1[7] = high1;
init_cmd1[8] = low1;
HAL_StatusTypeDef ret1 = MP3_SendCommand(init_cmd1, sizeof(init_cmd1));
elog_d("MP3", "开启声音命令返回值: %d", ret1);
// 延时等待模块响应
HAL_Delay(100);
// 命令2选择TF卡作为音源 (0x09 - 0x00 - 0x00 - 0x02)
uint8_t init_cmd2[] = {0x7E, 0xFF, 0x06, 0x09, 0x00, 0x00, 0x02, 0xFE, 0xF0, 0xEF};
uint8_t high2, low2;
MP3_CalcChecksum(init_cmd2, 7, &high2, &low2);
init_cmd2[7] = high2;
init_cmd2[8] = low2;
HAL_StatusTypeDef ret2 = MP3_SendCommand(init_cmd2, sizeof(init_cmd2));
elog_d("MP3", "选择TF卡命令返回值: %d", ret2);
if (ret1 == HAL_OK && ret2 == HAL_OK) {
elog_i("MP3", "MP3模块初始化完成");
return HAL_OK;
} else {
elog_e("MP3", "MP3模块初始化失败");
return HAL_ERROR;
}
}
/**
* @brief 按索引播放指定曲目
* @param index 曲目索引 (1-9999)
* @return HAL状态
*/
HAL_StatusTypeDef MP3_Play(uint16_t index)
{
if (index == 0 || index > 9999) {
elog_e("MP3", "无效的曲目索引: %d", index);
return HAL_ERROR;
}
// 构建播放命令0x7E 0xFF 0x06 0x12 0x00 high(索引) low(索引) checksum1 checksum2 0xEF
uint8_t play_cmd[10];
play_cmd[0] = MP3_HEADER;
play_cmd[1] = MP3_VERSION;
play_cmd[2] = MP3_LENGTH;
play_cmd[3] = MP3_CMD_PLAY_INDEX;
play_cmd[4] = 0x00;
play_cmd[5] = (index >> 8) & 0xFF; // 索引高字节
play_cmd[6] = index & 0xFF; // 索引低字节
// 计算校验和
uint8_t high, low;
MP3_CalcChecksum(play_cmd, 7, &high, &low);
play_cmd[7] = high;
play_cmd[8] = low;
play_cmd[9] = MP3_FOOTER;
// 发送命令
HAL_StatusTypeDef ret = MP3_SendCommand(play_cmd, sizeof(play_cmd));
if (ret == HAL_OK) {
elog_d("MP3", "播放曲目 %d", index);
} else {
elog_e("MP3", "播放曲目 %d 失败", index);
}
return ret;
}
/**
* @brief 停止播放
* @return HAL状态
*/
HAL_StatusTypeDef MP3_Stop(void)
{
// 构建停止命令0x7E 0xFF 0x06 0x16 0x00 0x00 0x00 checksum1 checksum2 0xEF
uint8_t stop_cmd[10];
stop_cmd[0] = MP3_HEADER;
stop_cmd[1] = MP3_VERSION;
stop_cmd[2] = MP3_LENGTH;
stop_cmd[3] = MP3_CMD_STOP;
stop_cmd[4] = 0x00;
stop_cmd[5] = 0x00;
stop_cmd[6] = 0x00;
// 计算校验和
uint8_t high, low;
MP3_CalcChecksum(stop_cmd, 7, &high, &low);
stop_cmd[7] = high;
stop_cmd[8] = low;
stop_cmd[9] = MP3_FOOTER;
HAL_StatusTypeDef ret = MP3_SendCommand(stop_cmd, sizeof(stop_cmd));
if (ret == HAL_OK) {
elog_d("MP3", "停止播放");
} else {
elog_e("MP3", "停止播放失败");
}
return ret;
}
/**
* @brief 暂停播放
* @return HAL状态
*/
HAL_StatusTypeDef MP3_Pause(void)
{
// 构建暂停命令0x7E 0xFF 0x06 0x0E 0x00 0x00 0x00 checksum1 checksum2 0xEF
uint8_t pause_cmd[10];
pause_cmd[0] = MP3_HEADER;
pause_cmd[1] = MP3_VERSION;
pause_cmd[2] = MP3_LENGTH;
pause_cmd[3] = MP3_CMD_PAUSE;
pause_cmd[4] = 0x00;
pause_cmd[5] = 0x00;
pause_cmd[6] = 0x00;
// 计算校验和
uint8_t high, low;
MP3_CalcChecksum(pause_cmd, 7, &high, &low);
pause_cmd[7] = high;
pause_cmd[8] = low;
pause_cmd[9] = MP3_FOOTER;
HAL_StatusTypeDef ret = MP3_SendCommand(pause_cmd, sizeof(pause_cmd));
if (ret == HAL_OK) {
elog_d("MP3", "暂停播放");
} else {
elog_e("MP3", "暂停播放失败");
}
return ret;
}
/**
* @brief 设置音量
* @param volume 音量值 (0-30)
* @return HAL状态
*/
HAL_StatusTypeDef MP3_SetVolume(uint8_t volume)
{
if (volume > 30) {
volume = 30; // 最大音量30
}
// 构建音量设置命令0x7E 0xFF 0x06 0x06 0x00 0x00 volume checksum1 checksum2 0xEF
uint8_t vol_cmd[10];
vol_cmd[0] = MP3_HEADER;
vol_cmd[1] = MP3_VERSION;
vol_cmd[2] = MP3_LENGTH;
vol_cmd[3] = MP3_CMD_SET_VOLUME;
vol_cmd[4] = 0x00;
vol_cmd[5] = 0x00;
vol_cmd[6] = volume;
// 计算校验和
uint8_t high, low;
MP3_CalcChecksum(vol_cmd, 7, &high, &low);
vol_cmd[7] = high;
vol_cmd[8] = low;
vol_cmd[9] = MP3_FOOTER;
HAL_StatusTypeDef ret = MP3_SendCommand(vol_cmd, sizeof(vol_cmd));
if (ret == HAL_OK) {
elog_d("MP3", "设置音量: %d", volume);
} else {
elog_e("MP3", "设置音量失败");
}
return ret;
}
/**
* @brief 设置音源
* @param source 音源类型 (1=U盘, 2=SD卡/TF卡)
* @return HAL状态
*/
HAL_StatusTypeDef MP3_SetSource(uint8_t source)
{
if (source != MP3_SOURCE_U_DISK && source != MP3_SOURCE_SD_CARD) {
elog_e("MP3", "无效的音源类型: %d", source);
return HAL_ERROR;
}
// 构建音源设置命令0x7E 0xFF 0x06 0x09 0x00 0x00 source checksum1 checksum2 0xEF
uint8_t source_cmd[10];
source_cmd[0] = MP3_HEADER;
source_cmd[1] = MP3_VERSION;
source_cmd[2] = MP3_LENGTH;
source_cmd[3] = MP3_CMD_SET_SOURCE;
source_cmd[4] = 0x00;
source_cmd[5] = 0x00;
source_cmd[6] = source;
// 计算校验和
uint8_t high, low;
MP3_CalcChecksum(source_cmd, 7, &high, &low);
source_cmd[7] = high;
source_cmd[8] = low;
source_cmd[9] = MP3_FOOTER;
HAL_StatusTypeDef ret = MP3_SendCommand(source_cmd, sizeof(source_cmd));
if (ret == HAL_OK) {
elog_d("MP3", "设置音源: %d", source);
} else {
elog_e("MP3", "设置音源失败");
}
return ret;
}

View File

@@ -98,7 +98,7 @@ void MX_USART3_UART_Init(void)
/* USER CODE END USART3_Init 1 */ /* USER CODE END USART3_Init 1 */
huart3.Instance = USART3; huart3.Instance = USART3;
huart3.Init.BaudRate = 115200; huart3.Init.BaudRate = 9600; // MP3模块通常使用9600波特率
huart3.Init.WordLength = UART_WORDLENGTH_8B; huart3.Init.WordLength = UART_WORDLENGTH_8B;
huart3.Init.StopBits = UART_STOPBITS_1; huart3.Init.StopBits = UART_STOPBITS_1;
huart3.Init.Parity = UART_PARITY_NONE; huart3.Init.Parity = UART_PARITY_NONE;

View File

@@ -0,0 +1,153 @@
# MP3语音播放集成总结
## 语音文件列表
| 编号 | 文件名 | 语音内容 | 触发场景 |
|------|--------|----------|----------|
| 001 | 001.mp3 | 欢迎使用智能按摩器 | 系统启动初始化完成 |
| 002 | 002.mp3 | 定时10分钟 | TIME_KEY设定10分钟 |
| 003 | 003.mp3 | 定时20分钟 | TIME_KEY设定20分钟 |
| 004 | 004.mp3 | 定时30分钟 | TIME_KEY设定30分钟 |
| 005 | 005.mp3 | 定时已取消 | TIME_KEY取消定时 |
| 006 | 006.mp3 | 按摩已停止 | 运行中按TIME_KEY取消 |
| 007 | 007.mp3 | 按摩开始 | M_KEY从0档启动 |
| 008 | 008.mp3 | 一档 | 升档或降档到1档 |
| 009 | 009.mp3 | 二档 | 升档或降档到2档 |
| 010 | 010.mp3 | 三档 | 升档到3档 |
| 011 | 011.mp3 | 已到最大档位 | 3档时再按M_KEY |
| 012 | 012.mp3 | 一档 | 降档从2档到1档 |
| 013 | 013.mp3 | 二档 | 降档从3档到2档 |
| 014 | 014.mp3 | 按摩停止 | M_KEYC7降档到0档 |
| 015 | 015.mp3 | 加热已开启 | HOT_KEY开启加热 |
| 016 | 016.mp3 | 加热已关闭 | HOT_KEY关闭加热 |
| 017 | 017.mp3 | 请先设定时间 | 未定时操作按键 |
| 018 | 018.mp3 | 定时结束,按摩结束 | 倒计时自动结束 |
## 代码修改位置
### 1. MP3任务初始化 (freertos.c:734)
```c
// 播放欢迎语音
MP3_Play(1);
```
### 2. TIME_KEY控制 - 时间设定 (freertos.c:502-538)
```c
// 播放定时语音
if (timer_minutes == 10) {
MP3_Play(2); // 定时10分钟
} else if (timer_minutes == 20) {
MP3_Play(3); // 定时20分钟
} else if (timer_minutes == 30) {
MP3_Play(4); // 定时30分钟
} else if (timer_minutes == 0) {
MP3_Play(5); // 定时已取消
}
// 运行中取消定时
MP3_Play(6); // 按摩已停止
```
### 3. M_KEY控制 - 加档 (freertos.c:437-456)
```c
// 从0档启动
if (Motor_GetGear() == 0) {
MP3_Play(7); // 按摩开始
} else {
// 已到最大档位
if (old_gear == 3) {
MP3_Play(11); // 已到最大档位
} else {
// 档位变化语音
MP3_Play(8); // 一档
MP3_Play(9); // 二档
MP3_Play(10); // 三档
}
}
// 未定时错误提示
MP3_Play(17); // 请先设定时间
```
### 4. M_KEYC7控制 - 降档 (freertos.c:458-477)
```c
// 降档语音
if (new_gear == 0) {
MP3_Play(14); // 按摩停止
} else if (new_gear == 1) {
MP3_Play(12); // 一档
} else if (new_gear == 2) {
MP3_Play(13); // 二档
}
// 未定时错误提示
MP3_Play(17); // 请先设定时间
```
### 5. HOT_KEY控制 - 加热开关 (freertos.c:479-499)
```c
if (hot_state) {
MP3_Play(15); // 加热已开启
} else {
MP3_Play(16); // 加热已关闭
}
// 未定时错误提示
MP3_Play(17); // 请先设定时间
```
### 6. Timer_Tick定时器 (freertos.c:221-245)
```c
// 时间到自动停止
if (remaining_seconds == 0) {
MP3_Play(18); // 定时结束,按摩结束
}
```
## MP3驱动API
### 初始化函数
```c
HAL_StatusTypeDef MP3_Init(void);
```
- 初始化MP3模块
- 设置音量30选择TF卡音源
### 播放控制
```c
HAL_StatusTypeDef MP3_Play(uint16_t index); // 播放指定曲目 (1-9999)
HAL_StatusTypeDef MP3_Stop(void); // 停止播放
HAL_StatusTypeDef MP3_Pause(void); // 暂停播放
```
### 参数设置
```c
HAL_StatusTypeDef MP3_SetVolume(uint8_t volume); // 设置音量 (0-30)
HAL_StatusTypeDef MP3_SetSource(uint8_t source); // 设置音源 (1=U盘, 2=SD卡)
```
## MP3命令格式
10字节固定格式:
```
字节0: 0x7E 帧头
字节1: 0xFF 版本号
字节2: 0x06 数据长度
字节3: 命令码 (0x06=音量, 0x09=音源, 0x12=播放索引)
字节4: 0x00 反馈
字节5: 数据高字节
字节6: 数据低字节
字节7: 校验和高位
字节8: 校验和低位
字节9: 0xEF 帧尾
```
校验和算法: `0x10000 - sum(字节1~6)`
## 注意事项
1. 确保TF卡中音频文件命名为001.mp3, 002.mp3, ... 格式
2. MP3模块需等待2秒上电稳定
3. USART3波特率设置为9600
4. 首次播放建议测试`MP3_Play(1)`是否正常工作
5. 所有语音播放函数已集成到相应按键处理逻辑中

View File

@@ -0,0 +1,209 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MP3播放函数使用示例
展示如何在代码中使用封装好的MP3播放函数
"""
# ==========================================
# C代码使用示例
# ==========================================
example_code = """
// 在 freertos.c 中使用 MP3 播放函数
#include "mp3_driver.h"
// ==========================================
// 1. 初始化MP3模块 (在 MP3 任务中)
// ==========================================
void MP3(void *argument) {
// 等待模块上电稳定
osDelay(2000);
// 初始化MP3模块
HAL_StatusTypeDef ret = MP3_Init();
if (ret != HAL_OK) {
elog_e("MP3", "MP3模块初始化失败");
return;
}
// ... 其他代码
}
// ==========================================
// 2. 播放指定曲目
// ==========================================
// 在 Sensor 任务中播放语音
MP3_Play(1); // 播放第1首: "欢迎使用智能按摩器"
MP3_Play(2); // 播放第2首: "定时10分钟"
MP3_Play(3); // 播放第3首: "定时20分钟"
// ... 依此类推
// ==========================================
// 3. 其他常用函数
// ==========================================
MP3_Stop(); // 停止播放
MP3_Pause(); // 暂停播放
MP3_SetVolume(20); // 设置音量 (0-30)
MP3_SetSource(MP3_SOURCE_SD_CARD); // 选择TF卡音源
// ==========================================
// 4. 实际应用示例:时间设定
// ==========================================
if (timer_minutes == 10) {
MP3_Play(2); // "定时10分钟"
} else if (timer_minutes == 20) {
MP3_Play(3); // "定时20分钟"
} else if (timer_minutes == 30) {
MP3_Play(4); // "定时30分钟"
} else if (timer_minutes == 0) {
MP3_Play(5); // "定时已取消"
}
// ==========================================
// 5. 实际应用示例:档位控制
// ==========================================
if (current_gear == 1) {
MP3_Play(8); // "一档"
} else if (current_gear == 2) {
MP3_Play(9); // "二档"
} else if (current_gear == 3) {
MP3_Play(10); // "三档"
}
// ==========================================
// 6. 实际应用示例:加热控制
// ==========================================
if (hot_state) {
MP3_Play(15); // "加热已开启"
} else {
MP3_Play(16); // "加热已关闭"
}
// ==========================================
// 7. 实际应用示例:定时结束
// ==========================================
if (remaining_seconds == 0) {
MP3_Play(18); // "定时结束,按摩结束"
Motor_SetGear(0); // 停止电机
// ... 其他清理工作
}
"""
# ==========================================
# 语音文件映射表
# ==========================================
voice_mapping = """
+------+--------------------------+
| 编号 | 语音内容 |
+------+--------------------------+
| 001 | 欢迎使用智能按摩器 |
| 002 | 定时10分钟 |
| 003 | 定时20分钟 |
| 004 | 定时30分钟 |
| 005 | 定时已取消 |
| 006 | 按摩已停止 |
| 007 | 按摩开始 |
| 008 | 一档 |
| 009 | 二档 |
| 010 | 三档 |
| 011 | 已到最大档位 (可选) |
| 012 | 一档 (降档用) |
| 013 | 二档 (降档用) |
| 014 | 按摩停止 |
| 015 | 加热已开启 |
| 016 | 加热已关闭 |
| 017 | 请先设定时间 |
| 018 | 定时结束,按摩结束 |
+------+--------------------------+
"""
# ==========================================
# MP3命令格式说明
# ==========================================
command_format = """
MP3命令格式 (10字节):
====================================================
字节 | 内容 | 说明
-----+---------------+---------------------------
0 | 0x7E | 帧头 (固定)
1 | 0xFF | 版本号 (固定)
2 | 0x06 | 数据长度 (固定)
3 | 命令码 | 0x06=音量, 0x09=音源, 0x12=播放索引
4 | 0x00 | 反馈 (固定)
5 | 数据高字节 | 根据命令不同
6 | 数据低字节 | 根据命令不同
7 | 校验和高位 | 计算得出
8 | 校验和低位 | 计算得出
9 | 0xEF | 帧尾 (固定)
====================================================
校验和计算方法:
1. 从索引1 (0xFF) 开始累加到索引6 (数据低字节)
2. 用 0x10000 减去累加结果
3. 高位在前,低位在后
示例 (播放文件1):
数据: 0xFF 0x06 0x12 0x00 0x00 0x01
求和: 0xFF + 0x06 + 0x12 + 0x00 + 0x00 + 0x01 = 0x0118
校验: 0x10000 - 0x0118 = 0xFEE8
高位: 0xFE, 低位: 0xE8
完整命令: 0x7E 0xFF 0x06 0x12 0x00 0x00 0x01 0xFE 0xE8 0xEF
"""
if __name__ == "__main__":
print("=" * 70)
print("MP3播放函数使用示例".center(70))
print("=" * 70)
print("\n" + "=" * 70)
print("1. C代码使用示例")
print("=" * 70)
print(example_code)
print("\n" + "=" * 70)
print("2. 语音文件映射表")
print("=" * 70)
print(voice_mapping)
print("\n" + "=" * 70)
print("3. MP3命令格式说明")
print("=" * 70)
print(command_format)
print("\n" + "=" * 70)
print("4. API 函数列表")
print("=" * 70)
print("""
HAL_StatusTypeDef MP3_Init(void);
- 初始化MP3模块
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_Play(uint16_t index);
- 播放指定曲目 (索引范围: 1-9999)
- 参数: index - 曲目编号
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_Stop(void);
- 停止当前播放
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_Pause(void);
- 暂停当前播放
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_SetVolume(uint8_t volume);
- 设置音量 (范围: 0-30)
- 参数: volume - 音量值
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
HAL_StatusTypeDef MP3_SetSource(uint8_t source);
- 设置音源
- 参数: source - MP3_SOURCE_U_DISK(1) 或 MP3_SOURCE_SD_CARD(2)
- 返回: HAL_OK(成功) 或 HAL_ERROR(失败)
""")
print("=" * 70)

View File

@@ -0,0 +1,222 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
音频文件按时间戳排序并重命名工具
功能:
1. 查找指定路径下的MP3文件
2. 按文件创建时间从先到后排序
3. 显示排序后的文件列表
4. 用户可指定起始编号(如0001或0002)
5. 重命名文件为0001.mp3, 0002.mp3等格式
"""
import os
import sys
import glob
from pathlib import Path
def find_mp3_files(directory):
"""查找指定目录下的所有MP3文件"""
mp3_files = glob.glob(os.path.join(directory, "*.mp3"))
return mp3_files
def is_timestamp_filename(filename):
"""判断文件名是否为时间戳格式(纯数字.mp3)"""
# 去除扩展名
name_without_ext = os.path.splitext(filename)[0]
# 检查是否为纯数字且至少为10位(时间戳)
return name_without_ext.isdigit() and len(name_without_ext) >= 10
def is_numbered_filename(filename):
"""判断文件名是否为编号格式(如0001.mp3)"""
# 去除扩展名
name_without_ext = os.path.splitext(filename)[0]
# 检查是否为纯数字长度为3-4位(编号格式)
return name_without_ext.isdigit() and len(name_without_ext) >= 3 and len(name_without_ext) <= 4
def filter_timestamp_files(files):
"""筛选出时间戳格式的MP3文件跳过编号格式的文件"""
timestamp_files = []
skipped_files = []
for file_path in files:
filename = os.path.basename(file_path)
if is_numbered_filename(filename):
skipped_files.append(filename)
elif is_timestamp_filename(filename):
timestamp_files.append(file_path)
else:
skipped_files.append(filename)
return timestamp_files, skipped_files
def sort_files_by_time(files):
"""按文件创建时间从先到后排序"""
# 获取文件信息并按创建时间排序
file_info = []
for file_path in files:
stat = os.stat(file_path)
file_info.append({
'path': file_path,
'name': os.path.basename(file_path),
'ctime': stat.st_ctime # 创建时间
})
# 按创建时间升序排序
sorted_files = sorted(file_info, key=lambda x: x['ctime'])
return sorted_files
def display_file_list(sorted_files, skipped_files):
"""显示排序后的文件列表"""
print("\n" + "=" * 80)
if skipped_files:
print(f"已跳过 {len(skipped_files)} 个非时间戳格式的文件:")
for filename in skipped_files:
print(f" 跳过: {filename}")
print()
print(f"找到 {len(sorted_files)} 个时间戳格式的MP3文件按创建时间排序:")
print("=" * 80)
for i, file_info in enumerate(sorted_files, 1):
import time
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(file_info['ctime']))
print(f"{i:3d}. {file_info['name']:<50} 创建时间: {time_str}")
print("=" * 80)
def get_start_number():
"""获取用户指定的起始编号"""
while True:
try:
user_input = input("\n请输入起始编号(如1或2将转换为0001/0002格式): ").strip()
if not user_input:
print("输入不能为空,请重新输入!")
continue
start_num = int(user_input)
if start_num < 1 or start_num > 9999:
print("编号必须在1-9999之间请重新输入!")
continue
return start_num
except ValueError:
print("请输入有效的数字!")
def rename_files(sorted_files, start_number):
"""重命名文件为编号格式"""
print("\n开始重命名文件...")
print("=" * 80)
success_count = 0
error_count = 0
for i, file_info in enumerate(sorted_files, start_number):
old_path = file_info['path']
directory = os.path.dirname(old_path)
new_name = f"{i:04d}.mp3"
new_path = os.path.join(directory, new_name)
try:
os.rename(old_path, new_path)
print(f"{file_info['name']} -> {new_name}")
success_count += 1
except Exception as e:
print(f"{file_info['name']} 重命名失败: {e}")
error_count += 1
print("=" * 80)
print(f"重命名完成! 成功: {success_count}, 失败: {error_count}")
def confirm_operation():
"""确认是否执行重命名操作"""
while True:
user_input = input("\n确认要重命名这些文件吗? (y/n): ").strip().lower()
if user_input in ['y', 'yes']:
return True
elif user_input in ['n', 'no']:
return False
else:
print("请输入 y 或 n")
def main():
print("=" * 80)
print("音频文件按时间戳排序并重命名工具".center(80))
print("=" * 80)
# 获取目标目录
while True:
directory = input("\n请输入要处理的MP3文件所在路径: ").strip()
directory = directory.strip('"').strip("'") # 去除可能的引号
if not directory:
print("路径不能为空!")
continue
if not os.path.isdir(directory):
print(f"路径不存在: {directory}")
continue
break
# 查找MP3文件
mp3_files = find_mp3_files(directory)
if not mp3_files:
print(f"\n在路径 {directory} 中未找到MP3文件!")
return
# 筛选时间戳格式文件,跳过编号格式文件
timestamp_files, skipped_files = filter_timestamp_files(mp3_files)
if not timestamp_files:
print(f"\n在路径 {directory} 中未找到时间戳格式的MP3文件!")
if skipped_files:
print(f"但找到 {len(skipped_files)} 个已编号格式的文件:")
for filename in skipped_files:
print(f" {filename}")
return
# 按时间排序
sorted_files = sort_files_by_time(timestamp_files)
# 显示文件列表
display_file_list(sorted_files, skipped_files)
# 获取起始编号
start_number = get_start_number()
# 预览重命名结果
print("\n重命名预览:")
print("-" * 80)
for i, file_info in enumerate(sorted_files, start_number):
new_name = f"{i:04d}.mp3"
print(f"{i:4d}. {file_info['name']} -> {new_name}")
print("-" * 80)
# 确认操作
if confirm_operation():
rename_files(sorted_files, start_number)
else:
print("\n操作已取消!")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n程序被用户中断!")
sys.exit(0)
except Exception as e:
print(f"\n程序执行出错: {e}")
import traceback
traceback.print_exc()
sys.exit(1)

View File

@@ -0,0 +1,118 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MP3命令校验和验证工具
验证校验和算法是否正确
"""
def calc_checksum(cmd):
"""
计算MP3命令校验和
算法:从版本号(索引1)开始到数据结束所有字节相加后取反再加1
返回:高位和低位
"""
sum_val = 0
# 从版本号(索引1)开始累加,到数据结束(索引6)
for i in range(1, 7):
sum_val += cmd[i]
# 取反加1 (即0x10000 - sum_val)
sum_val = 0x10000 - sum_val
# 高位在前,低位在后
high = (sum_val >> 8) & 0xFF
low = sum_val & 0xFF
return high, low
def verify_command(cmd, name):
"""验证命令的校验和是否正确"""
print(f"\n{'='*60}")
print(f"验证命令: {name}")
print(f"{'='*60}")
# 提取原始数据
data = cmd[:7]
original_high = cmd[7]
original_low = cmd[8]
# 显示命令字节
print(f"命令字节: ", end="")
for i, byte in enumerate(cmd):
print(f"0x{byte:02X} ", end="")
if i == 3:
print("| ", end="")
print()
# 显示数据部分
print(f"数据部分 (索引1-6): ", end="")
for i in range(1, 7):
print(f"0x{cmd[i]:02X} ", end="")
if i == 3:
print("| ", end="")
print()
# 显示求和过程
sum_val = sum(cmd[1:7])
print(f"\n求和: {sum_val} (0x{sum_val:04X})")
print(f"取反加1: 0x10000 - 0x{sum_val:04X} = 0x{0x10000 - sum_val:04X}")
# 计算校验和
calc_high, calc_low = calc_checksum(cmd)
print(f"\n计算校验和: 高位=0x{calc_high:02X}, 低位=0x{calc_low:02X}")
print(f"原始校验和: 高位=0x{original_high:02X}, 低位=0x{original_low:02X}")
# 验证
if calc_high == original_high and calc_low == original_low:
print("[OK] 校验和验证通过!")
return True
else:
print("[FAIL] 校验和验证失败!")
return False
def main():
print("="*60)
print("MP3命令校验和验证工具")
print("="*60)
# 命令1开启声音 (音量30)
# 0x7E 0xFF 0x06 0x06 0x00 0x00 0x1E 0xFE 0xD7 0xEF
cmd1 = [0x7E, 0xFF, 0x06, 0x06, 0x00, 0x00, 0x1E, 0xFE, 0xD7, 0xEF]
verify_command(cmd1, "开启声音 (音量30)")
# 命令2选择TF卡
# 0x7E 0xFF 0x06 0x09 0x00 0x00 0x02 0xFE 0xF0 0xEF
cmd2 = [0x7E, 0xFF, 0x06, 0x09, 0x00, 0x00, 0x02, 0xFE, 0xF0, 0xEF]
verify_command(cmd2, "选择TF卡")
# 命令3播放文件1
# 0x7E 0xFF 0x06 0x12 0x00 0x00 0x01 0xFE 0xE8 0xEF
cmd3 = [0x7E, 0xFF, 0x06, 0x12, 0x00, 0x00, 0x01, 0xFE, 0xE8, 0xEF]
verify_command(cmd3, "播放文件1")
# 测试:生成其他曲目的播放命令
print(f"\n{'='*60}")
print("生成其他曲目的播放命令")
print(f"{'='*60}")
for index in [1, 10, 100, 9999]:
play_cmd = [0x7E, 0xFF, 0x06, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0xEF]
play_cmd[5] = (index >> 8) & 0xFF
play_cmd[6] = index & 0xFF
high, low = calc_checksum(play_cmd)
play_cmd[7] = high
play_cmd[8] = low
print(f"\n曲目 {index:4d}: ", end="")
for byte in play_cmd:
print(f"0x{byte:02X} ", end="")
print()
print(f"\n{'='*60}")
if __name__ == "__main__":
main()

View File

@@ -2,21 +2,23 @@
text_device_name=智能按摩仪 text_device_name=智能按摩仪
text_loading = 加载中 text_loading = 加载中
text_massage_on = 按摩启动 text_massage_off = 按摩:设备停止
text_massage_off = 按摩停止
text_heat_on = 加热启动 text_heat_on = 加热:开始加热
text_heat_off = 加热停止 text_heat_off = 加热:停止加热
text_gear = %d级 text_gear_1 =按摩:当前低档位
text_time = %d分钟 text_gear_2 =按摩:当前中档位
text_cont = 持续 text_gear_3 =按摩:当前高档位
text_remaining = 剩余%d分
text_running = 运行中 text_time_1 =时间:设定10分钟
text_paused = 已暂停 text_time_2 =时间:设定20分钟
text_ended = 已结束 text_time_3 =时间:设定30分钟
text_remaining = 时间:剩余%d分
text_stop = 机器运行已停止

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

281
README.md
View File

@@ -1,95 +1,280 @@
# SmartMassager_STM32 # SmartMassager_STM32
STM32 智能按摩控制系统项目 STM32 智能按摩控制系统项目
## 项目概述 ## 项目概述
本项目基于 STM32 微控制器开发的智能按摩控制系统集成了串口屏显示、FreeRTOS 实时操作系统、多种传感器控制等功能 本项目基于 STM32 微控制器开发的智能按摩控制系统集成了串口屏显示、FreeRTOS 实时操作系统、MP3语音提示、定时倒计时、多档位电机控制等功能。系统支持完整的按摩操作流程包括定时设定、档位调节、加热控制等并通过语音提示提供友好的用户交互体验
## 硬件平台 ## 硬件平台
- 主控芯片STM32 系列微控制器 - **主控芯片**: STM32F103XE 系列微控制器
- 显示屏JC 系列串口屏(通过 USART1 通信) - **显示屏**: JC 系列串口屏(通过 USART1 通信,波特率 115200
- 操作系统FreeRTOS 实时操作系统 - **MP3模块**: 通过 USART3 通信(波特率 9600
- **电机控制**: TIM3 PWM 输出(三档 PWM75% / 85% / 95%
- **加热控制**: TIM1 CH1 PWM 输出(占空比 300
- **操作系统**: FreeRTOS 实时操作系统
- **调试工具**: ST-LINK
## 功能特性
### 核心功能
#### 1. 定时倒计时系统
- 支持 0→10→20→30→0 分钟循环定时设定
- 实时倒计时显示,动态更新剩余时间
- 定时结束自动停止所有设备(电机、加热)
- 运行中按下定时键可取消定时并停止
#### 2. MP3语音提示系统
- 18个预设语音文件覆盖所有操作场景
- 支持播放控制:播放指定索引曲目、停止、暂停、设置音量、切换音源
- 自动校验和计算,确保通信可靠性
- 语音提示包括:欢迎语、定时设定、档位变化、加热开关、错误提示等
#### 3. 多档位电机控制
- 三档力度调节75%(一档) / 85%(二档) / 95%(三档)
- 启动冲击时间1500ms
- 降档平滑过渡800ms 适应时间 + 100% PWM 冲击
- 精确的 PWM 通道控制:`Motor_StartAll()` / `Motor_StopAll()`
- 优化一档力度从 65% 提升至 75%,改善低速按摩效果
#### 4. 加热控制
- PWM 控制加热输出TIM1 CH1占空比 300
- 开关状态实时显示
- 配合语音提示反馈
#### 5. 分区屏幕显示
- **Zone1**: 显示时间状态(停止 / 设定时间 / 剩余时间倒计时)
- **Zone2**: 显示按摩档位(停止 / 一档 / 二档 / 三档)
- **Zone3**: 显示加热状态(开启 / 关闭)
- 动态生成剩余时间 GBK 字符串("时间:剩余XX分"
- 状态变化时才更新屏幕,优化性能
#### 6. LED指示灯
- **RUN_LED**: 运行时以 500ms 周期闪烁,停止时熄灭
- **ERR_LED**: 无效操作时亮 1 秒提示
- 低电平点亮控制方式
#### 7. 操作安全机制
- 所有操作(按摩、加热)需先设定时间,否则提示错误
- 未设定时间操作时播放语音"请先设定时间"并点亮 ERR_LED
- 完善的错误处理和用户提示
## 按键功能
| 按键 | 功能 | 说明 |
|------|------|------|
| **TIME_KEY** | 定时设定 | 循环切换0→10→20→30→0 分钟 |
| **M_KEY** | 加档 | 从当前档位递增0→1→2→3到三档时提示"已到最大档位" |
| **M_KEYC7** | 减档 | 从当前档位递减3→2→1→0减到0档时停止按摩 |
| **HOT_KEY** | 加热开关 | 切换加热开/关状态 |
## 软件架构 ## 软件架构
### 主要模块 ### 主要模块
1. **屏幕驱动模块** (`screen.c`) 1. **MP3驱动模块** (`mp3_driver.c`)
- 完整的 MP3 驱动协议实现
- 支持播放控制命令封装
- 自动校验和计算
2. **屏幕驱动模块** (`screen.c`)
- 串口屏底层驱动实现 - 串口屏底层驱动实现
- 支持基本绘图功能(点、线、矩形、圆形 - 分区显示功能Zone1/Zone2/Zone3
- GBK 字符显示支持 - GBK 字符显示支持
- 基本绘图功能(点、线、矩形、圆形)
- 图片显示功能 - 图片显示功能
2. **任务调度模块** 3. **电机驱动模块** (`motor_driver.c`)
- 基于 FreeRTOS 的多任务管理 - 三档 PWM 控制
- 各子系统任务协调运行 - 启动加速功能
- 降档冲击优化
- 精确通道控制
3. **日志系统** 4. **GBK文本模块** (`gbk_text.c`)
- GBK 编码文本常量定义
- 支持动态时间字符串生成
5. **任务调度模块** (`freertos.c`)
- 基于 FreeRTOS 的多任务管理
- Motor 任务500ms 周期)
- Screen 任务10ms 周期)
- 定时器 Tick 处理1秒周期
6. **日志系统**
- 集成 EasyLogger 日志框架 - 集成 EasyLogger 日志框架
- 多级别日志输出 - 多级别日志输出
### 核心功能 ### 核心 API
#### 定时器 API
- `Timer_CanOperate()` - 检查定时器是否可操作
- `Timer_GetRemainingSeconds()` - 获取剩余秒数
- `Timer_GetMinutes()` - 获取设定的分钟数
- `Timer_IsRunning()` - 检查定时器是否运行中
#### MP3播放 API
- `MP3_Play(uint8_t index)` - 播放指定索引曲目
- `MP3_Stop()` - 停止播放
- `MP3_Pause()` - 暂停播放
- `MP3_SetVolume(uint8_t volume)` - 设置音量0-30
- `MP3_SetSource(uint8_t source)` - 设置音源
#### 电机控制 API
- `Motor_SetGear(uint8_t gear)` - 设置档位0-3
- `Motor_StartAll()` - 启动所有电机通道
- `Motor_StopAll()` - 停止所有电机通道
- `Motor_GetCurrentGear()` - 获取当前档位
#### 屏幕控制 API #### 屏幕控制 API
- `Screen_Init()` - 屏幕初始化 - `Screen_Init()` - 屏幕初始化
- `Screen_Clear()` - 清屏 - `Screen_Clear()` - 清屏
- `Screen_DrawText16_GBK()` - 绘制16号字体GBK文本 - `Screen_DrawText16_GBK()` - 绘制16号字体GBK文本
- `Screen_DrawText24_GBK()` - 绘制24号字体GBK文本 - `Screen_DrawText24_GBK()` - 绘制24号字体GBK文本
- `Screen_Box()` - 绘制矩形框 - `Screen_ShowZone1_Time()` - 显示Zone1时间状态
- `Screen_Line()` - 绘制直线 - `Screen_ShowZone2_Gear()` - 显示Zone2档位状态
- `Screen_ShowZone3_Heat()` - 显示Zone3加热状态
#### 电机控制
- 多档位速度控制
- 启动加速功能
- 过流、超温保护
#### 按键处理
- 防抖处理
- 四个功能按键:
- M_KEY: 加档
- M_KEYC7: 减档
- HOT_KEY: 热敷开关
- TIME_KEY: 定时功能
#### 音频播放
- MP3音频播放支持
- TF卡文件读取
## 开发环境 ## 开发环境
- IDE: STM32CubeIDE - **IDE**: STM32CubeIDE
- 编译器: GCC ARM - **编译器**: GCC ARM
- 调试工具: ST-LINK - **构建工具**: CMake
- **调试工具**: ST-LINK
## 构建说明 ## 构建说明
1. 克隆项目到本地 1. 克隆项目到本地
2. 使用 STM32CubeIDE 导入项目 2. 使用 STM32CubeIDE 导入项目
3. 配置目标芯片型号 3. 配置目标芯片型号STM32F103XE
4. 编译项目 4. 编译项目
5. 通过 ST-LINK 下载到目标板 5. 通过 ST-LINK 下载到目标板
### 依赖文件
- `CMakeLists.txt` - CMake 构建配置
- `CMakePresets.json` - CMake 预设配置
- `STM32F103XX_FLASH.ld` - 链接脚本
- `startup_stm32f103xe.s` - 启动文件
## 使用说明 ## 使用说明
1. 上电后系统自动初始化 ### 基本操作流程
2. 屏幕显示设备名称和加载界面
3. 初始化完成后进入主操作界面
4. 通过按键控制按摩功能
## 注意事项 1. **上电启动**
- 系统自动初始化
- 播放欢迎语音MP3文件1
- 屏幕显示加载界面
- 请确保电源电压符合要求 2. **设定时间**
- 避免在潮湿环境中使用 - 按下 `TIME_KEY` 选择定时时长10/20/30分钟
- 定期检查连接线路 - 屏幕显示设定时间
- 如遇异常情况请立即断电 - 播放相应语音提示
## 版本信息 3. **启动按摩**
- 按下 `M_KEY` 启动按摩(一档)
- RUN_LED 开始闪烁
- 开始倒计时
- 当前版本: v1.0.0 4. **调节档位**
- 发布日期: 2024年 - `M_KEY` 加档(一档→二档→三档)
-`M_KEYC7` 减档(三档→二档→一档→停止)
- 每次操作播放语音提示
5. **加热控制**
-`HOT_KEY` 开关加热
- 屏幕显示加热状态
- 播放语音提示
6. **定时结束**
- 倒计时归零
- 自动停止所有设备
- 播放"定时结束,按摩结束"语音
### 语音文件索引
| 索引 | 语音内容 | 触发时机 |
|------|----------|----------|
| 1 | 欢迎语 | 系统启动 |
| 2 | 设定10分钟 | 定时键按下 |
| 3 | 设定20分钟 | 定时键按下 |
| 4 | 设定30分钟 | 定时键按下 |
| 5 | 取消定时 | 定时键取消 |
| 6 | 按摩已停止 | 取消定时 |
| 7 | 按摩开始 | 首次启动按摩 |
| 8 | 一档 | 加档到一档 |
| 9 | 二档 | 加档到二档 |
| 10 | 三档 | 加档到三档 |
| 11 | 已到最大档位 | 尝试加到四档 |
| 12 | 一档 | 降档到一档 |
| 13 | 二档 | 降档到二档 |
| 14 | 按摩停止 | 降档到零档 |
| 15 | 加热已开 | 打开加热 |
| 16 | 加热已关 | 关闭加热 |
| 17 | 请先设定时间 | 未设定时间操作 |
| 18 | 定时结束,按摩结束 | 倒计时结束 |
## 项目结构
```
SmartMassager_STM32/
├── Core/ # 核心代码
│ ├── Inc/ # 头文件
│ │ ├── mp3_driver.h # MP3驱动头文件
│ │ ├── motor_driver.h # 电机驱动头文件
│ │ ├── screen.h # 屏幕驱动头文件
│ │ ├── gbk_text.h # GBK文本定义
│ │ └── freertos.h # FreeRTOS配置
│ └── Src/ # 源文件
│ ├── mp3_driver.c # MP3驱动实现
│ ├── motor_driver.c # 电机驱动实现
│ ├── screen.c # 屏幕驱动实现
│ ├── gbk_text.c # GBK文本实现
│ ├── freertos.c # FreeRTOS任务实现
│ └── main.c # 主程序
├── Drivers/ # STM32 HAL驱动
├── Middlewares/ # 中间件FreeRTOS
├── Development_Docs/ # 开发文档
│ └── Serial_Screen_Docs/ # 串口屏相关文档
│ ├── LIST/ # MP3集成文档和脚本
│ └── 测试记录/ # 功能测试记录
├── build/ # 构建输出目录
├── CMakeLists.txt # CMake配置
├── anmo.ioc # STM32CubeMX项目文件
└── README.md # 项目说明文档
```
## 文档资源
### 开发文档
- `Development_Docs/Serial_Screen_Docs/LIST/mp3_integration_summary.md` - MP3集成文档
- `Development_Docs/Serial_Screen_Docs/LIST/mp3_usage_example.py` - MP3使用示例
- `Development_Docs/Serial_Screen_Docs/LIST/rename_mp3_files.py` - MP3文件重命名脚本
- `Development_Docs/Serial_Screen_Docs/LIST/verify_mp3_checksum.py` - MP3校验和验证脚本
### 测试记录
- `Development_Docs/测试记录/` - 功能测试记录目录
## 技术支持 ## 技术支持
如有问题请联系开发团队或查看相关技术文档。 如有问题请联系开发团队或查看相关技术文档。
## 版本信息
- **当前版本**: v2.0.0
- **发布日期**: 2025年2月
- **主要更新**:
- ✅ 完整实现定时倒计时功能
- ✅ 集成MP3语音提示系统
- ✅ 优化电机控制(一档力度提升、降档平滑)
- ✅ 新增分区屏幕显示
- ✅ 实现LED运行指示和错误提示
- ✅ 完善操作安全机制
## 许可证
本项目仅供学习和参考使用。
---
**注意**: 使用前请确保所有硬件连接正确,电源电压符合要求,避免在潮湿环境中使用。