diff --git a/CMakeLists.txt b/CMakeLists.txt index b9ff499..87c8d4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ target_sources(${CMAKE_PROJECT_NAME} PRIVATE Core/Src/motor_driver.c Core/Src/screen.c Core/Src/gbk_text.c + Core/Src/mp3_driver.c ) # Add include paths diff --git a/Core/Inc/gbk_text.h b/Core/Inc/gbk_text.h index 4052d84..e4acda9 100644 --- a/Core/Inc/gbk_text.h +++ b/Core/Inc/gbk_text.h @@ -6,46 +6,46 @@ // GBK文本数组声明(定义在gbk_text.c中) extern const uint8_t text_device_name_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_heat_on_GBK[]; extern const uint8_t text_heat_off_GBK[]; -extern const uint8_t text_gear_GBK[]; -extern const uint8_t text_time_GBK[]; -extern const uint8_t text_cont_GBK[]; +extern const uint8_t text_gear_1_GBK[]; +extern const uint8_t text_gear_2_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_running_GBK[]; -extern const uint8_t text_paused_GBK[]; -extern const uint8_t text_ended_GBK[]; +extern const uint8_t text_stop_GBK[]; // GBK文本长度宏(预计算,不含\0) #define text_device_name_LEN (10) #define text_loading_LEN (6) -#define text_massage_on_LEN (8) -#define text_massage_off_LEN (8) -#define text_heat_on_LEN (8) -#define text_heat_off_LEN (8) -#define text_gear_LEN (4) -#define text_time_LEN (6) -#define text_cont_LEN (4) -#define text_remaining_LEN (8) -#define text_running_LEN (6) -#define text_paused_LEN (6) -#define text_ended_LEN (6) +#define text_massage_off_LEN (13) +#define text_heat_on_LEN (13) +#define text_heat_off_LEN (13) +#define text_gear_1_LEN (15) +#define text_gear_2_LEN (15) +#define text_gear_3_LEN (15) +#define text_time_1_LEN (15) +#define text_time_2_LEN (15) +#define text_time_3_LEN (15) +#define text_remaining_LEN (13) +#define text_stop_LEN (14) // 兼容旧代码的别名 #define text_device_name text_device_name_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_heat_on text_heat_on_GBK #define text_heat_off text_heat_off_GBK -#define text_gear text_gear_GBK -#define text_time text_time_GBK -#define text_cont text_cont_GBK +#define text_gear_1 text_gear_1_GBK +#define text_gear_2 text_gear_2_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_running text_running_GBK -#define text_paused text_paused_GBK -#define text_ended text_ended_GBK +#define text_stop text_stop_GBK #endif // __GBK_TEXT_H__ diff --git a/Core/Inc/mp3_driver.h b/Core/Inc/mp3_driver.h new file mode 100644 index 0000000..d68e9e1 --- /dev/null +++ b/Core/Inc/mp3_driver.h @@ -0,0 +1,45 @@ +#ifndef __MP3_DRIVER_H +#define __MP3_DRIVER_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "stm32f1xx_hal.h" +#include + +/* 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 */ diff --git a/Core/Src/freertos.c b/Core/Src/freertos.c index 98bca6c..efeb5bd 100644 --- a/Core/Src/freertos.c +++ b/Core/Src/freertos.c @@ -23,7 +23,6 @@ #include "main.h" #include "task.h" - /* Private includes ----------------------------------------------------------*/ /* USER CODE BEGIN Includes */ #include "elog.h" @@ -36,7 +35,7 @@ #include #include #include - +#include "mp3_driver.h" /* USER CODE END Includes */ /* Private typedef -----------------------------------------------------------*/ @@ -64,7 +63,7 @@ PUTCHAR_PROTOTYPE { /* USER CODE END PM */ /* Private variables ---------------------------------------------------------*/ -/* USER CODE BEGIN Variables */ + /* USER CODE BEGIN Variables */ /* 按键状态位: bit0..bit3 分别对应 M__KEY, M__KEYC7, HOT_KEY, TIME_KEY, * 为1表示按下 */ @@ -72,8 +71,14 @@ volatile uint8_t key_state = 0; volatile uint8_t key_state_prev = 0; /* 上一次的按键状态,用于检测按键变化 */ /* 设备状态标志:0表示关闭,1表示打开 */ -static uint8_t hot_state = 0; /* HOT 设备状态 */ -static uint8_t time_state = 0; /* TIME_KEY 时间状态 */ +static uint8_t hot_state = 0; /* HOT 设备状态 */ + +/* 定时器相关变量 */ +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 */ /* Definitions for defaultTask */ @@ -125,6 +130,127 @@ const osEventFlagsAttr_t init_ok_attributes = {.name = "init_ok"}; /* Private function prototypes -----------------------------------------------*/ /* 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 */ void StartDefaultTask(void *argument); @@ -228,10 +354,11 @@ void StartDefaultTask(void *argument) { 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_ShowInZone(1, text_ended, text_ended_LEN); + // Screen_DrawText16V_GBK(1, 30, text_stop_massage, text_stop_massage_LEN, + // 15); + Screen_ShowInZone(1, text_stop, text_stop_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", "完成所有的初始化"); @@ -315,43 +442,158 @@ void Sensor(void *argument) { /* ===== M__KEY (bit0) 控制 ===== */ if (key_pressed & (1 << 0)) { - /* M__KEY 按下:加档(提高转速) */ - elog_i("Key", "M__KEY按下 - 加档"); - if (Motor_GetGear() == 0) { - Motor_StartupBoost(); + if (Timer_CanOperate()) { // 添加判断 + /* M__KEY 按下:加档(提高转速) */ + elog_i("Key", "M__KEY按下 - 加档"); + 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 { - 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) 控制 ===== */ if (key_pressed & (1 << 1)) { - /* M__KEYC7 按下:降档(降低转速) */ - elog_i("Key", "M__KEYC7按下 - 降档"); - Motor_GearDown(); + if (Timer_CanOperate()) { // 添加判断 + /* M__KEYC7 按下:降档(降低转速) */ + 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) 控制 ===== */ if (key_pressed & (1 << 2)) { - /* HOT_KEY 按下:切换热功能 */ - hot_state = !hot_state; - if (hot_state) { - __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 300); - elog_d("Hot", "设置PWM为300"); + if (Timer_CanOperate()) { // 添加判断 + /* HOT_KEY 按下:切换热功能 */ + hot_state = !hot_state; + if (hot_state) { + __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 { - __HAL_TIM_SET_COMPARE(&htim1, TIM_CHANNEL_1, 0); /* 关闭加热 */ - elog_d("Hot", "设置PWM为0"); + elog_w("Key", "HOT_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灭 } } /* ===== TIME_KEY (bit3) 控制 ===== */ if (key_pressed & (1 << 3)) { - /* TIME_KEY 按下:切换定时 */ - time_state = !time_state; - if (time_state) { - // TODO: 添加定时启动逻辑 + /* TIME_KEY 按下:循环设定时间 0→10→20→30→0 */ + if (!is_running) { + if (timer_minutes >= 30) { + 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 { - // 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(); + /* 初始化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); /* Infinite loop */ + static uint32_t tick_counter = 0; + for (;;) { - /* 电机任务主要功能: - * - 监控电机状态(超温保护、过流保护等) - * - 定期打印状态信息 - * - 响应紧急停止信号 - */ + tick_counter++; - /* 每1秒打印一次电机状态 */ - uint8_t current_gear = Motor_GetGear(); - elog_i("Motor", "当前档位: %u (0=停止,1=低,2=中,3=高)", current_gear); + // 每1000ms调用一次Timer_Tick(每2个循环周期) + if (tick_counter % 2 == 0) { + Timer_Tick(); // 倒计时处理 + } - HAL_GPIO_TogglePin(RUN_LED_GPIO_Port, RUN_LED_Pin); - osDelay(1000); + // RUN_LED闪烁控制:运行时500ms翻转一次 (LED低电平点亮) + 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 BEGIN Header_Screen */ + /** * @brief Function implementing the Screen_Tsak thread. * @param argument: Not used @@ -403,6 +683,9 @@ void Motor(void *argument) { /* USER CODE END Header_Screen */ void Screen(void *argument) { /* USER CODE BEGIN Screen */ + static uint8_t last_hot_state = 0; // 记录上一次的热状态 + static uint8_t last_gear = 0xFF; // 记录上一次的挡位(初始为无效值) + elog_d("Init", "Screen task started"); 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); /* Infinite loop */ 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 */ } @@ -424,32 +749,54 @@ void Screen(void *argument) { * @retval None */ /* USER CODE END Header_MP3 */ + void MP3(void *argument) { /* USER CODE BEGIN MP3 */ elog_d("Init", "MP3 task started"); - uint8_t init_cmd1[] = {0x7E, 0xFF, 0x06, 0x06, 0x00, - 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 + elog_d("MP3", "USART3波特率: 9600"); - // 等待模块上电稳定 - osDelay(2000); + // uint8_t init_cmd1[] = {0x7E, 0xFF, 0x06, 0x06, 0x00, + // 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", "模块初始化完成"); + MP3_Play(1); osEventFlagsSet(init_okHandle, 1 << 4); /* Infinite loop */ + for (;;) { - // 发送播放命令 - HAL_UART_Transmit(&huart3, play_cmd, sizeof(play_cmd), 100); osDelay(10000); } diff --git a/Core/Src/gbk_text.c b/Core/Src/gbk_text.c index 950a21f..ef31acb 100644 --- a/Core/Src/gbk_text.c +++ b/Core/Src/gbk_text.c @@ -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 const uint8_t text_loading_GBK[] = {0xBC, 0xD3, 0xD4, 0xD8, 0xD6, 0xD0}; -// 对应文本(UTF8):按摩启动 -// 对应GBK编码:0xB0, 0xB4, 0xC4, 0xA6, 0xC6, 0xF4, 0xB6, 0xAF -const uint8_t text_massage_on_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0xC6, 0xF4, 0xB6, 0xAF}; +// 对应文本(UTF8):按摩:设备停止 +// 对应GBK编码:0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xC9, 0xE8, 0xB1, 0xB8, 0xCD, 0xA3, 0xD6, 0xB9 +const uint8_t text_massage_off_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xC9, 0xE8, 0xB1, 0xB8, 0xCD, 0xA3, 0xD6, 0xB9}; -// 对应文本(UTF8):按摩停止 -// 对应GBK编码:0xB0, 0xB4, 0xC4, 0xA6, 0xCD, 0xA3, 0xD6, 0xB9 -const uint8_t text_massage_off_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0xCD, 0xA3, 0xD6, 0xB9}; +// 对应文本(UTF8):加热:开始加热 +// 对应GBK编码:0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xBF, 0xAA, 0xCA, 0xBC, 0xBC, 0xD3, 0xC8, 0xC8 +const uint8_t text_heat_on_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xBF, 0xAA, 0xCA, 0xBC, 0xBC, 0xD3, 0xC8, 0xC8}; -// 对应文本(UTF8):加热启动 -// 对应GBK编码:0xBC, 0xD3, 0xC8, 0xC8, 0xC6, 0xF4, 0xB6, 0xAF -const uint8_t text_heat_on_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0xC6, 0xF4, 0xB6, 0xAF}; +// 对应文本(UTF8):加热:停止加热 +// 对应GBK编码:0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xCD, 0xA3, 0xD6, 0xB9, 0xBC, 0xD3, 0xC8, 0xC8 +const uint8_t text_heat_off_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0x3A, 0xCD, 0xA3, 0xD6, 0xB9, 0xBC, 0xD3, 0xC8, 0xC8}; -// 对应文本(UTF8):加热停止 -// 对应GBK编码:0xBC, 0xD3, 0xC8, 0xC8, 0xCD, 0xA3, 0xD6, 0xB9 -const uint8_t text_heat_off_GBK[] = {0xBC, 0xD3, 0xC8, 0xC8, 0xCD, 0xA3, 0xD6, 0xB9}; +// 对应文本(UTF8):按摩:当前低档位 +// 对应GBK编码:0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB5, 0xCD, 0xB5, 0xB5, 0xCE, 0xBB +const uint8_t text_gear_1_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB5, 0xCD, 0xB5, 0xB5, 0xCE, 0xBB}; -// 对应文本(UTF8):%d级 -// 对应GBK编码:0x25, 0x64, 0xBC, 0xB6 -const uint8_t text_gear_GBK[] = {0x25, 0x64, 0xBC, 0xB6}; +// 对应文本(UTF8):按摩:当前中档位 +// 对应GBK编码:0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xD6, 0xD0, 0xB5, 0xB5, 0xCE, 0xBB +const uint8_t text_gear_2_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xD6, 0xD0, 0xB5, 0xB5, 0xCE, 0xBB}; -// 对应文本(UTF8):%d分钟 -// 对应GBK编码:0x25, 0x64, 0xB7, 0xD6, 0xD6, 0xD3 -const uint8_t text_time_GBK[] = {0x25, 0x64, 0xB7, 0xD6, 0xD6, 0xD3}; +// 对应文本(UTF8):按摩:当前高档位 +// 对应GBK编码:0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB8, 0xDF, 0xB5, 0xB5, 0xCE, 0xBB +const uint8_t text_gear_3_GBK[] = {0xB0, 0xB4, 0xC4, 0xA6, 0x3A, 0xB5, 0xB1, 0xC7, 0xB0, 0xB8, 0xDF, 0xB5, 0xB5, 0xCE, 0xBB}; -// 对应文本(UTF8):持续 -// 对应GBK编码:0xB3, 0xD6, 0xD0, 0xF8 -const uint8_t text_cont_GBK[] = {0xB3, 0xD6, 0xD0, 0xF8}; +// 对应文本(UTF8):时间:设定10分钟 +// 对应GBK编码:0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x31, 0x30, 0xB7, 0xD6, 0xD6, 0xD3 +const uint8_t text_time_1_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x31, 0x30, 0xB7, 0xD6, 0xD6, 0xD3}; -// 对应文本(UTF8):剩余%d分 -// 对应GBK编码:0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6 -const uint8_t text_remaining_GBK[] = {0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6}; +// 对应文本(UTF8):时间:设定20分钟 +// 对应GBK编码:0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x32, 0x30, 0xB7, 0xD6, 0xD6, 0xD3 +const uint8_t text_time_2_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x32, 0x30, 0xB7, 0xD6, 0xD6, 0xD3}; -// 对应文本(UTF8):运行中 -// 对应GBK编码:0xD4, 0xCB, 0xD0, 0xD0, 0xD6, 0xD0 -const uint8_t text_running_GBK[] = {0xD4, 0xCB, 0xD0, 0xD0, 0xD6, 0xD0}; +// 对应文本(UTF8):时间:设定30分钟 +// 对应GBK编码:0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x33, 0x30, 0xB7, 0xD6, 0xD6, 0xD3 +const uint8_t text_time_3_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xC9, 0xE8, 0xB6, 0xA8, 0x33, 0x30, 0xB7, 0xD6, 0xD6, 0xD3}; -// 对应文本(UTF8):已暂停 -// 对应GBK编码:0xD2, 0xD1, 0xD4, 0xDD, 0xCD, 0xA3 -const uint8_t text_paused_GBK[] = {0xD2, 0xD1, 0xD4, 0xDD, 0xCD, 0xA3}; +// 对应文本(UTF8):时间:剩余%d分 +// 对应GBK编码:0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6 +const uint8_t text_remaining_GBK[] = {0xCA, 0xB1, 0xBC, 0xE4, 0x3A, 0xCA, 0xA3, 0xD3, 0xE0, 0x25, 0x64, 0xB7, 0xD6}; -// 对应文本(UTF8):已结束 -// 对应GBK编码:0xD2, 0xD1, 0xBD, 0xE1, 0xCA, 0xF8 -const uint8_t text_ended_GBK[] = {0xD2, 0xD1, 0xBD, 0xE1, 0xCA, 0xF8}; +// 对应文本(UTF8):机器运行已停止 +// 对应GBK编码:0xBB, 0xFA, 0xC6, 0xF7, 0xD4, 0xCB, 0xD0, 0xD0, 0xD2, 0xD1, 0xCD, 0xA3, 0xD6, 0xB9 +const uint8_t text_stop_GBK[] = {0xBB, 0xFA, 0xC6, 0xF7, 0xD4, 0xCB, 0xD0, 0xD0, 0xD2, 0xD1, 0xCD, 0xA3, 0xD6, 0xB9}; diff --git a/Core/Src/motor_driver.c b/Core/Src/motor_driver.c index 0618c9c..667cc3b 100644 --- a/Core/Src/motor_driver.c +++ b/Core/Src/motor_driver.c @@ -20,13 +20,15 @@ /* Private function prototypes -----------------------------------------------*/ +void Motor_StartAll(void); +void Motor_StopAll(void); /* PWM parameters */ #define PWM_ARR 99 #define PWM_STEPS (PWM_ARR + 1) /* 三档占空比(百分比),索引为 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 = 高 */ 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; } -/* 将两个电机都设置为指定占空比(只使用 TIM4_CH2, TIM4_CH4 输出,CH1/CH3 停止以保持单方向) */ +/* 将两个电机都设置为指定占空比(使用 AT8236-MS H桥驱动) */ static void set_both_motors_percent(uint8_t percent) { uint32_t cmp = percent_to_compare(percent); - /* 停止并关闭 CH1、使用 CH2 输出 */ - //HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1); + /* 电机1: TIM4_CH1->AIN1, TIM4_CH2->AIN2 + * 正转: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_2, 100); + __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_2, 0); - /* 停止并关闭 CH3、使用 CH4 输出 */ - //HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_3); + /* 电机2: TIM4_CH3->BIN1, TIM4_CH4->BIN2 + * 正转: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_4, 100); + __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, 0); } -/* 启动时短暂提升到 75% 并保持 500ms,然后降到一档(档位1) */ +/* 启动时短暂提升到 100% 并保持 500ms,然后降到一档(档位1) */ 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); 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]); } @@ -61,9 +70,13 @@ void Motor_SetGear(uint8_t gear) { if (gear > 3) gear = 3; current_gear = gear; if (current_gear == 0) { - // set_both_motors_percent(0); + // 完全停止所有PWM输出 Motor_Stop(); + elog_d("Motor", "Motor_SetGear: 档位0, 停止"); } 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]); } } @@ -72,10 +85,15 @@ void Motor_SetGear(uint8_t gear) { void Motor_NextGear(void) { current_gear++; if (current_gear > 3) current_gear = 0; + if (current_gear == 0) { - // set_both_motors_percent(0); + // 完全停止所有PWM输出 Motor_Stop(); + elog_d("Motor", "Motor_NextGear: 档位0, 停止"); } 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]); } } @@ -87,14 +105,33 @@ void Motor_GearUp(void) { /* 向下减档(循环) */ void Motor_GearDown(void) { + uint8_t old_gear = current_gear; + if (current_gear == 0) { current_gear = 3; } else { current_gear--; } + + elog_d("Motor", "Motor_GearDown: %d -> %d", old_gear, current_gear); + if (current_gear == 0) { - set_both_motors_percent(0); + // 完全停止所有PWM输出 + Motor_Stop(); + elog_d("Motor", "档位0, 停止"); } 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]); } } @@ -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) { // 所有引脚输出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); } diff --git a/Core/Src/mp3_driver.c b/Core/Src/mp3_driver.c new file mode 100644 index 0000000..ea1eb85 --- /dev/null +++ b/Core/Src/mp3_driver.c @@ -0,0 +1,265 @@ +#include "mp3_driver.h" +#include "elog.h" +#include + +/* 外部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; +} diff --git a/Core/Src/usart.c b/Core/Src/usart.c index 39d06eb..66a81ab 100644 --- a/Core/Src/usart.c +++ b/Core/Src/usart.c @@ -98,7 +98,7 @@ void MX_USART3_UART_Init(void) /* USER CODE END USART3_Init 1 */ huart3.Instance = USART3; - huart3.Init.BaudRate = 115200; + huart3.Init.BaudRate = 9600; // MP3模块通常使用9600波特率 huart3.Init.WordLength = UART_WORDLENGTH_8B; huart3.Init.StopBits = UART_STOPBITS_1; huart3.Init.Parity = UART_PARITY_NONE; diff --git a/Development_Docs/Serial_Screen_Docs/LIST/mp3_integration_summary.md b/Development_Docs/Serial_Screen_Docs/LIST/mp3_integration_summary.md new file mode 100644 index 0000000..f0bcd4d --- /dev/null +++ b/Development_Docs/Serial_Screen_Docs/LIST/mp3_integration_summary.md @@ -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. 所有语音播放函数已集成到相应按键处理逻辑中 diff --git a/Development_Docs/Serial_Screen_Docs/LIST/mp3_usage_example.py b/Development_Docs/Serial_Screen_Docs/LIST/mp3_usage_example.py new file mode 100644 index 0000000..5a8cc8b --- /dev/null +++ b/Development_Docs/Serial_Screen_Docs/LIST/mp3_usage_example.py @@ -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) diff --git a/Development_Docs/Serial_Screen_Docs/LIST/rename_mp3_files.py b/Development_Docs/Serial_Screen_Docs/LIST/rename_mp3_files.py new file mode 100644 index 0000000..8f81b9f --- /dev/null +++ b/Development_Docs/Serial_Screen_Docs/LIST/rename_mp3_files.py @@ -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) diff --git a/Development_Docs/Serial_Screen_Docs/LIST/verify_mp3_checksum.py b/Development_Docs/Serial_Screen_Docs/LIST/verify_mp3_checksum.py new file mode 100644 index 0000000..dfa0f53 --- /dev/null +++ b/Development_Docs/Serial_Screen_Docs/LIST/verify_mp3_checksum.py @@ -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() diff --git a/Development_Docs/Serial_Screen_Docs/UTF8-GBK/text_utf8.txt b/Development_Docs/Serial_Screen_Docs/UTF8-GBK/text_utf8.txt index 2268edc..4fc7d6b 100644 --- a/Development_Docs/Serial_Screen_Docs/UTF8-GBK/text_utf8.txt +++ b/Development_Docs/Serial_Screen_Docs/UTF8-GBK/text_utf8.txt @@ -2,21 +2,23 @@ text_device_name=智能按摩仪 text_loading = 加载中 -text_massage_on = 按摩启动 -text_massage_off = 按摩停止 +text_massage_off = 按摩:设备停止 -text_heat_on = 加热启动 -text_heat_off = 加热停止 +text_heat_on = 加热:开始加热 +text_heat_off = 加热:停止加热 -text_gear = %d级 -text_time = %d分钟 -text_cont = 持续 +text_gear_1 =按摩:当前低档位 +text_gear_2 =按摩:当前中档位 +text_gear_3 =按摩:当前高档位 -text_remaining = 剩余%d分 -text_running = 运行中 -text_paused = 已暂停 -text_ended = 已结束 +text_time_1 =时间:设定10分钟 +text_time_2 =时间:设定20分钟 +text_time_3 =时间:设定30分钟 + +text_remaining = 时间:剩余%d分 + +text_stop = 机器运行已停止 diff --git a/Development_Docs/测试记录/语音列表.png b/Development_Docs/测试记录/语音列表.png new file mode 100644 index 0000000..84ee89c Binary files /dev/null and b/Development_Docs/测试记录/语音列表.png differ diff --git a/Development_Docs/测试记录/运行日志.png b/Development_Docs/测试记录/运行日志.png new file mode 100644 index 0000000..77285e5 Binary files /dev/null and b/Development_Docs/测试记录/运行日志.png differ diff --git a/README.md b/README.md index ed1501d..7435bac 100644 --- a/README.md +++ b/README.md @@ -1,95 +1,280 @@ # SmartMassager_STM32 -STM32 智能按摩椅控制系统项目 +STM32 智能按摩器控制系统项目 ## 项目概述 -本项目基于 STM32 微控制器开发的智能按摩椅控制系统,集成了串口屏显示、FreeRTOS 实时操作系统、多种传感器控制等功能。 +本项目基于 STM32 微控制器开发的智能按摩器控制系统,集成了串口屏显示、FreeRTOS 实时操作系统、MP3语音提示、定时倒计时、多档位电机控制等功能。系统支持完整的按摩操作流程,包括定时设定、档位调节、加热控制等,并通过语音提示提供友好的用户交互体验。 ## 硬件平台 -- 主控芯片:STM32 系列微控制器 -- 显示屏:JC 系列串口屏(通过 USART1 通信) -- 操作系统:FreeRTOS 实时操作系统 +- **主控芯片**: STM32F103XE 系列微控制器 +- **显示屏**: JC 系列串口屏(通过 USART1 通信,波特率 115200) +- **MP3模块**: 通过 USART3 通信(波特率 9600) +- **电机控制**: TIM3 PWM 输出(三档 PWM:75% / 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 字符显示支持 + - 基本绘图功能(点、线、矩形、圆形) - 图片显示功能 -2. **任务调度模块** - - 基于 FreeRTOS 的多任务管理 - - 各子系统任务协调运行 +3. **电机驱动模块** (`motor_driver.c`) + - 三档 PWM 控制 + - 启动加速功能 + - 降档冲击优化 + - 精确通道控制 -3. **日志系统** +4. **GBK文本模块** (`gbk_text.c`) + - GBK 编码文本常量定义 + - 支持动态时间字符串生成 + +5. **任务调度模块** (`freertos.c`) + - 基于 FreeRTOS 的多任务管理 + - Motor 任务(500ms 周期) + - Screen 任务(10ms 周期) + - 定时器 Tick 处理(1秒周期) + +6. **日志系统** - 集成 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 - `Screen_Init()` - 屏幕初始化 - `Screen_Clear()` - 清屏 - `Screen_DrawText16_GBK()` - 绘制16号字体GBK文本 - `Screen_DrawText24_GBK()` - 绘制24号字体GBK文本 -- `Screen_Box()` - 绘制矩形框 -- `Screen_Line()` - 绘制直线 - -#### 电机控制 -- 多档位速度控制 -- 启动加速功能 -- 过流、超温保护 - -#### 按键处理 -- 防抖处理 -- 四个功能按键: - - M_KEY: 加档 - - M_KEYC7: 减档 - - HOT_KEY: 热敷开关 - - TIME_KEY: 定时功能 - -#### 音频播放 -- MP3音频播放支持 -- TF卡文件读取 +- `Screen_ShowZone1_Time()` - 显示Zone1时间状态 +- `Screen_ShowZone2_Gear()` - 显示Zone2档位状态 +- `Screen_ShowZone3_Heat()` - 显示Zone3加热状态 ## 开发环境 -- IDE: STM32CubeIDE -- 编译器: GCC ARM -- 调试工具: ST-LINK +- **IDE**: STM32CubeIDE +- **编译器**: GCC ARM +- **构建工具**: CMake +- **调试工具**: ST-LINK ## 构建说明 1. 克隆项目到本地 2. 使用 STM32CubeIDE 导入项目 -3. 配置目标芯片型号 +3. 配置目标芯片型号(STM32F103XE) 4. 编译项目 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 -- 发布日期: 2024年 +4. **调节档位** + - 按 `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/测试记录/` - 功能测试记录目录 ## 技术支持 -如有问题请联系开发团队或查看相关技术文档。 \ No newline at end of file +如有问题请联系开发团队或查看相关技术文档。 + +## 版本信息 + +- **当前版本**: v2.0.0 +- **发布日期**: 2025年2月 +- **主要更新**: + - ✅ 完整实现定时倒计时功能 + - ✅ 集成MP3语音提示系统 + - ✅ 优化电机控制(一档力度提升、降档平滑) + - ✅ 新增分区屏幕显示 + - ✅ 实现LED运行指示和错误提示 + - ✅ 完善操作安全机制 + +## 许可证 + +本项目仅供学习和参考使用。 + +--- + +**注意**: 使用前请确保所有硬件连接正确,电源电压符合要求,避免在潮湿环境中使用。 \ No newline at end of file