第05章 按键控制显示
约 2919 字大约 10 分钟
2026-01-26
5.1 公共层
5.1.1 调试模块
代码
从定位器项目中移植即可!
App_Debug.h
#ifndef __COM_DEBUG_H__
#define __COM_DEBUG_H__
#include <stdio.h>
#include <string.h>
#include "usart.h"
// 定义开启调试模式
#define DEBUG_MODE 1
#if DEBUG_MODE == 1
#define PRE_FILENAME (strrchr(__FILE__, '/') ? (strrchr(__FILE__, '/') + 1) : __FILE__)
#define FILENAME (strrchr(PRE_FILENAME, '\\') ? (strrchr(PRE_FILENAME, '\\') + 1) : PRE_FILENAME)
#define DEBUG_PRINTF(fmt, ...) printf("[%s:%d]" fmt, FILENAME, __LINE__, ##__VA_ARGS__)
#define DEBUG_PRINTLN(fmt, ...) printf("[%s:%d]" fmt "\n", FILENAME, __LINE__, ##__VA_ARGS__)
#else
#define DEBUG_PRINTF(fmt, ...)
#define DEBUG_PRINTLN(fmt, ...)
#endif
#endif /* __COM_DEBUG_H__ */App_Debug.c
#include "Com_Debug.h"
// 重定义 fputc
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}5.1.2 全局模块
代码
Com_Global.h
#ifndef __COM_GLOBAL_H__
#define __COM_GLOBAL_H__
#include <stdint.h>
#include "FreeRTOS.h"
#include "task.h"
// 宏定义:最大的目标角度
#define MAX_TARGET_ANGLE 7200.0f
// 宏定义:最小的目标角度
#define MIN_TARGET_ANGLE -7200.0f
// 宏定义:每次角度变化的步长
#define ANGLE_STEP 20.0f
// 宏定义:最大的目标速度
#define MAX_TARGET_SPEED 200.0f
// 宏定义:最小的目标速度
#define MIN_TARGET_SPEED 0.0f
// 宏定义:每次速度变化的步长
#define SPEED_STEP 10.0f
// 宏定义:Modbus ID 最大值
#define MAX_MODBUS_ID 247
// 宏定义:Modbus ID 最小值
#define MIN_MODBUS_ID 1
// 宏定义:默认的 Modbus ID
#define DEFAULT_MODBUS_ID 1
// 宏定义:最大的页码
#define MAX_PAGE 2
// 声明全局变量
extern float g_target_angle;
extern float g_current_angle;
extern float g_target_speed;
extern float g_current_speed;
extern uint8_t g_modbus_id;
extern uint8_t g_current_page;
// 全局声明ModbusID管理任务的句柄
extern TaskHandle_t modubsid_task_handle;
#endif /* __COM_GLOBAL_H__ */Com_Global.c
#include "Com_Global.h"
// 定义全局变量: 目标角度
float g_target_angle = 360.0f;
// 定义全局变量: 当前角度
float g_current_angle = 0.0f;
// 定义全局变量:目标速度,转速单位使用 转/分, RPM
float g_target_speed = 0.0f;
// 定义全局变量:当前速度,转速单位使用 转/分, RPM
float g_current_speed = 0.0f;
// 定义全局变量:Modbus ID (1~247)
uint8_t g_modbus_id = DEFAULT_MODBUS_ID;
// 定义全局变量:当前显示的页码(0~2)
uint8_t g_current_page = 0;5.2 硬件结构层
5.2.1 OLED 模块
① 原理图

② CubeMX 设置
设置 I2C2:

③ 代码
下载 OLED驱动代码,放入硬件结构层目录中,里面包括 Int_OLED.h、Int_OLED.c、Int_font.h 三个文件。
5.2.2 按键模块
① 原理图


② CubeMX 设置
设置4个按键对应的引脚为上拉输入模式:

③ 代码
Int_key.h
#ifndef __INT_KEY_H__
#define __INT_KEY_H__
#include "gpio.h"
/**
* @brief 检测按键否被触发
*
* @return uint8_t 0 表示没有按键被按下;1表示SW1被按下;2表示SW2被按下;3表示SW3被按下;4表示SW4被按下
*/
uint8_t Int_Key_IsDetect(void);
#endif /* __INT_KEY_H__ */Int_Key.c
#include "Int_Key.h"
/**
* @brief 检测按键否被触发
*
* @return uint8_t 0 表示没有按键被按下;1表示SW1被按下;2表示SW2被按下;3表示SW3被按下;4表示SW4被按下
*/
uint8_t Int_Key_IsDetect(void)
{
// 检测SW1是否被按下,对应的引脚是低电平
if (HAL_GPIO_ReadPin(KEY_SW1_GPIO_Port, KEY_SW1_Pin) == GPIO_PIN_RESET)
{
// 延时用于消抖
HAL_Delay(10);
// 再次检测SW1对应的引脚是否还是低电平
if (HAL_GPIO_ReadPin(KEY_SW1_GPIO_Port, KEY_SW1_Pin) == GPIO_PIN_RESET)
{
// 等待按键抬起,如果SW1引脚是低电平就一直循环,直到引脚获取到高电平
while (HAL_GPIO_ReadPin(KEY_SW1_GPIO_Port, KEY_SW1_Pin) == GPIO_PIN_RESET)
;
return 1;
}
}
// 检测SW2是否被按下,对应的引脚是低电平
if (HAL_GPIO_ReadPin(KEY_SW2_GPIO_Port, KEY_SW2_Pin) == GPIO_PIN_RESET)
{
// 延时用于消抖
HAL_Delay(10);
// 再次检测SW2对应的引脚是否还是低电平
if (HAL_GPIO_ReadPin(KEY_SW2_GPIO_Port, KEY_SW2_Pin) == GPIO_PIN_RESET)
{
// 等待按键抬起,如果SW2引脚是低电平就一直循环,直到引脚获取到高电平
while (HAL_GPIO_ReadPin(KEY_SW2_GPIO_Port, KEY_SW2_Pin) == GPIO_PIN_RESET)
;
return 2;
}
}
// 检测SW3是否被按下,对应的引脚是低电平
if (HAL_GPIO_ReadPin(KEY_SW3_GPIO_Port, KEY_SW3_Pin) == GPIO_PIN_RESET)
{
// 延时用于消抖
HAL_Delay(10);
// 再次检测SW3对应的引脚是否还是低电平
if (HAL_GPIO_ReadPin(KEY_SW3_GPIO_Port, KEY_SW3_Pin) == GPIO_PIN_RESET)
{
// 等待按键抬起,如果SW3引脚是低电平就一直循环,直到引脚获取到高电平
while (HAL_GPIO_ReadPin(KEY_SW3_GPIO_Port, KEY_SW3_Pin) == GPIO_PIN_RESET)
;
return 3;
}
}
// 检测SW4是否被按下,对应的引脚是低电平
if (HAL_GPIO_ReadPin(KEY_SW4_GPIO_Port, KEY_SW4_Pin) == GPIO_PIN_RESET)
{
// 延时用于消抖
HAL_Delay(10);
// 再次检测SW4对应的引脚是否还是低电平
if (HAL_GPIO_ReadPin(KEY_SW4_GPIO_Port, KEY_SW4_Pin) == GPIO_PIN_RESET)
{
// 等待按键抬起,如果SW4引脚是低电平就一直循环,直到引脚获取到高电平
while (HAL_GPIO_ReadPin(KEY_SW4_GPIO_Port, KEY_SW4_Pin) == GPIO_PIN_RESET)
;
return 4;
}
}
return 0;
}5.2.3 EEPROM 模块
① M24C02 介绍


② 原理图


③ CubeMX 设置
与 OLED 共同使用 I2C2:

④ 代码
从过去相关案例中复制,或者直接下载 EEPROM驱动代码,放入硬件结构层目录中,里面包括 Int_EEPROM.h、Int_EEPROM.c 两个文件。
5.3 应用层
5.3.1 显示应用
① 需求描述
第 1 页 (g_current_page 为 0) 显示效果:
尚硅谷电机项目
Tar Agl: 0.0
Cur Agl: 0.0第 2 页 (g_current_page 为 1) 显示效果:
尚硅谷电机项目
Tar Spd: 0.0
Cur Spd: 0.0第 3 页 (g_current_page 为 2) 显示效果:
尚硅谷电机项目
Modbus ID: 1② 代码
App_Display.h
#ifndef __APP_DISPLAY_H__
#define __APP_DISPLAY_H__
#include <stdio.h>
#include "Com_Global.h"
#include "Int_OLED.h"
/**
* @brief 初始化显示模块
*
*/
void App_Display_Init(void);
/**
* @brief 更新显示,需要被轮询调用
*
*/
void App_Display_Update(void);
#endif /* __APP_DISPLAY_H__ */App_Display.c
#include "App_Display.h"
// 定义字符串作为第二行显示的内容
uint8_t second_line[17] = {0};
// 定义字符串作为第三行显示的内容
uint8_t third_line[17] = {0};
// 静态函数:显示目标角度和当前角度
static void App_Display_ShowAngle(void)
{
// 显示第二行:目标角度
sprintf((char *)second_line, "Tar Agl:%8.1f", g_target_angle);
Int_OLED_ShowString(0, 16, second_line, 16, 1);
// 显示第三行:当前角度
sprintf((char *)third_line, "Cur Agl:%8.1f", g_current_angle);
Int_OLED_ShowString(0, 32, third_line, 16, 1);
}
// 静态函数:显示目标速度和当前速度
static void App_Display_ShowSpeed(void)
{
// 显示第二行:目标速度
sprintf((char *)second_line, "Tar Spd:%8.1f", g_target_speed);
Int_OLED_ShowString(0, 16, second_line, 16, 1);
// 显示第三行:当前速度
sprintf((char *)third_line, "Cur Spd:%8.1f", g_current_speed);
Int_OLED_ShowString(0, 32, third_line, 16, 1);
}
// 静态函数:显示Modbus ID
static void App_Display_ShowModbusID(void)
{
// 显示第二行:全部是空格
memset(second_line, ' ', 16);
second_line[16] = '\0';
Int_OLED_ShowString(0, 16, second_line, 16, 1);
// 显示第三行:Modbus ID
sprintf((char *)third_line, "Modbus ID:%6d", g_modbus_id);
Int_OLED_ShowString(0, 32, third_line, 16, 1);
}
/**
* @brief 初始化显示模块
*
*/
void App_Display_Init(void)
{
// 初始化OLED显示屏
Int_OLED_Init();
// 显示中文的项目标题, 标题前空半格
for (uint8_t i = 0; i < 7; i++)
{
Int_OLED_ShowChinese(i * 16 + 8, 0, i, 16, 1);
}
// // 刷新显示(向现实模块发送数据)
// Int_OLED_Refresh();
}
/**
* @brief 更新显示,需要被轮询调用
*
*/
void App_Display_Update(void)
{
// 根据页码显示内容
switch (g_current_page)
{
case 0:
// 显示第一页内容:目标角度和当前角度
App_Display_ShowAngle();
break;
case 1:
// 显示第二页内容:目标速度和当前速度
App_Display_ShowSpeed();
break;
case 2:
// 显示第三页内容:Modbus ID
App_Display_ShowModbusID();
break;
default:
break;
}
// 刷新显示(向现实模块发送数据)
Int_OLED_Refresh();
}5.3.2 按键控制应用
① 需求描述
| 按键 | KEY_SW1 | KEY_SW2 | KEY_SW3 | KEY_SW4 |
|---|---|---|---|---|
| 第一页 角度 | 目标角度+20 | 目标角度-20 | 电机启动 | 翻译 |
| 第二页 速度 | 目标转速+10 | 目标转速-10 | 电机启动 | 翻页 |
| 第三页 Modbus ID | ModbusID + 1 | Modbus ID -1 | 电机启动 | 翻页 |
② 代码
App_KeyControl.h
#ifndef __APP_KEYCONTROL_H__
#define __APP_KEYCONTROL_H__
#include "FreeRTOS.h"
#include "task.h"
#include "Com_Global.h"
#include "Com_Debug.h"
#include "Int_Key.h"
#include "Int_Motor.h"
/**
* @brief 按键控制,需要被轮询调用
*
*/
void App_KeyControl_Run(void);
#endif /* __APP_KEYCONTROL_H__ */App_KeyControl.c
#include "App_KeyControl.h"
/**
* @brief 按键控制,需要被轮询调用
*
*/
void App_KeyControl_Run(void)
{
// 检测按键
switch (Int_Key_IsDetect())
{
case 1:
// 检测到SW1,加操作
DEBUG_PRINTLN("SW1 pressed");
// 如果是第0页,目标角度增加
if (g_current_page == 0)
{
g_target_angle += 20.0f;
if (g_target_angle > MAX_TARGET_ANGLE)
{
g_target_angle = MAX_TARGET_ANGLE;
}
}
// 如果是第1页,目标速度增加
else if (g_current_page == 1)
{
g_target_speed += 10.0f;
if (g_target_speed > MAX_TARGET_SPEED)
{
g_target_speed = MAX_TARGET_SPEED;
}
}
// 如果是第2页,Modbus ID 增加
else if (g_current_page == 2)
{
g_modbus_id++;
if (g_modbus_id > MAX_MODBUS_ID)
{
g_modbus_id = MAX_MODBUS_ID;
}
// 向ModbusID管理任务发送通知
xTaskNotifyGive(modubsid_task_handle);
}
break;
case 2:
// 检测到SW2,减操作
DEBUG_PRINTLN("SW2 pressed");
// 如果是第0页,目标角度减少
if (g_current_page == 0)
{
g_target_angle -= 20.0f;
if (g_target_angle < MIN_TARGET_ANGLE)
{
g_target_angle = MIN_TARGET_ANGLE;
}
}
// 如果是第1页,目标速度减少
else if (g_current_page == 1)
{
g_target_speed -= 10.0f;
if (g_target_speed < MIN_TARGET_SPEED)
{
g_target_speed = MIN_TARGET_SPEED;
}
}
// 如果是第2页,Modbus ID 减少
else if (g_current_page == 2)
{
g_modbus_id--;
if (g_modbus_id < MIN_MODBUS_ID)
{
g_modbus_id = MIN_MODBUS_ID;
}
// 向ModbusID管理任务发送通知
xTaskNotifyGive(modubsid_task_handle);
}
break;
case 3:
// 检测到SW3,启动操作
DEBUG_PRINTLN("SW3 pressed");
break;
case 4:
// 检测到SW4,换页操作
DEBUG_PRINTLN("SW4 pressed");
g_current_page++;
if (g_current_page > MAX_PAGE)
{
g_current_page = 0;
}
break;
default:
break;
}
}5.3.3 Modbus应用
① 需求分析
1. 修改 Modbus ID 后,存入EEPROM中
2. 上电后先获取 EEPROM 中的 Modbus ID,赋值给全局变量 g_modbus_id② 代码
App_Modbus.h
#ifndef __APP_MODUBS_H__
#define __APP_MODUBS_H__
#include "Com_Debug.h"
#include "Com_Global.h"
#include "Int_EEPROM.h"
// 宏定义:Modubs ID的存储地址
#define MODUBS_ID_EEPROM_ADDR 0x00
/**
* @brief 设置Modubs ID
*
*/
void App_Modubs_SetID(void);
/**
* @brief 获取Modubs ID
*
*/
void App_Modubs_GetID(void);
#endif /* __APP_MODUBS_H__ */App_Modubs.c
#include "App_Modubs.h"
/**
* @brief 设置Modubs ID
*
*/
void App_Modubs_SetID(void)
{
// 将 Modbus ID 写入EEPROM
Int_EEPROM_WriteData(MODUBS_ID_EEPROM_ADDR, &g_modbus_id, 1);
}
/**
* @brief 获取Modubs ID
*
*/
void App_Modubs_GetID(void)
{
// 从 EEPROM 中读取 Modbus ID
Int_EEPROM_ReadData(MODUBS_ID_EEPROM_ADDR, &g_modbus_id, 1);
// 检查 Modbus ID 是否有效
if (g_modbus_id < MIN_MODBUS_ID || g_modbus_id > MAX_MODBUS_ID)
{
// 如果 ID 无效,设置为默认值
g_modbus_id = DEFAULT_MODBUS_ID;
}
}5.3.4 任务控制应用
代码
App_Task.h
#ifndef __APP_TASK_H__
#define __APP_TASK_H__
#include <stdio.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "Com_Debug.h"
#include "App_Display.h"
#include "App_KeyControl.h"
#include "App_Modubs.h"
/**
* @brief 启动 FreeRTOS 任务管理
*
*/
void App_Task_Start(void);
#endif /* __APP_TASK_H__ */App_Task.c
#include "App_Task.h"
// 显示任务 ------------------------------------
// 显示任务函数的原型
void disaply_task_callback(void *pvParameters);
// 显示任务名称
#define DISPLAY_TASK_NAME "display_task"
// 显示任务堆栈大小
#define DISPLAY_TASK_STACK_SIZE 512
// 显示任务的优先级
#define DISPLAY_TASK_PRIORITY 1
// 任务1的句柄
TaskHandle_t display_task_handle;
// 按键控制任务 ------------------------------------
// 按键控制任务函数的原型
void keycontrol_task_callback(void *pvParameters);
// 按键控制任务名称
#define KEYCONTROL_TASK_NAME "keycontrol_task"
// 按键控制任务堆栈大小
#define KEYCONTROL_TASK_STACK_SIZE 512
// 按键控制任务的优先级
#define KEYCONTROL_TASK_PRIORITY 2
// 任务2的句柄
TaskHandle_t keycontrol_task_handle;
// ModbusID管理任务 ------------------------------------
// ModbusID管理任务函数的原型
void modubsid_task_callback(void *pvParameters);
// ModbusID管理任务名称
#define MODUBSID_TASK_NAME "modubsid_task"
// ModbusID管理任务堆栈大小
#define MODUBSID_TASK_STACK_SIZE 512
// ModbusID管理任务的优先级
#define MODUBSID_TASK_PRIORITY 3
// 任务3的句柄
TaskHandle_t modubsid_task_handle;
/**
* @brief 启动 FreeRTOS 任务管理
*
*/
void App_Task_Start(void)
{
// 进入临界区
taskENTER_CRITICAL();
// 创建显示任务
xTaskCreate(disaply_task_callback, DISPLAY_TASK_NAME, DISPLAY_TASK_STACK_SIZE, NULL, DISPLAY_TASK_PRIORITY, &display_task_handle) == pdPASS ? printf("显示任务创建成功! \n") : printf("显示任务穿件失败! \n");
// 创建按键控制任务
xTaskCreate(keycontrol_task_callback, KEYCONTROL_TASK_NAME, KEYCONTROL_TASK_STACK_SIZE, NULL, KEYCONTROL_TASK_PRIORITY, &keycontrol_task_handle) == pdPASS ? printf("按键控制任务创建成功! \n") : printf("按键控制任务穿件失败! \n");
// 创建ModbusID管理任务
xTaskCreate(modubsid_task_callback, MODUBSID_TASK_NAME, MODUBSID_TASK_STACK_SIZE, NULL, MODUBSID_TASK_PRIORITY, &modubsid_task_handle) == pdPASS ? printf("ModbusID管理任务创建成功! \n") : printf("ModbusID管理任务穿件失败! \n");
taskEXIT_CRITICAL();
// 启动任务调度器 ( vTaskStartScheduler() 后面的代码不会被执行)
printf("任务调度器启动... \n");
vTaskStartScheduler();
}
// 显示任务函数的实现
void disaply_task_callback(void *pvParameters)
{
printf("显示任务启动... \n");
// 初始化显示模块
App_Display_Init();
while (1)
{
// 进入临界区,防止I2C通信被打断
taskENTER_CRITICAL();
// 更新显示
App_Display_Update();
// 退出临界区
taskEXIT_CRITICAL();
// 延时500ms
vTaskDelay(100);
}
}
// 按键控制任务函数的实现
void keycontrol_task_callback(void *pvParameters)
{
printf("按键控制任务启动... \n");
while (1)
{
// 检测按键
App_KeyControl_Run();
// 延时100ms
vTaskDelay(100);
}
}
// ModbusID管理任务函数的实现
void modubsid_task_callback(void *pvParameters)
{
printf("ModbusID管理任务启动... \n");
// // 将默认Modbus ID写入EEPROM; 只需要出厂的时候设置一次
// App_Modubs_SetID();
// 获取Modbus ID
App_Modubs_GetID();
while (1)
{
// 等待接收任务通知,接收不到一直阻塞
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 保存Modbus ID
App_Modubs_SetID();
}
}main.c
/* 代码省略 ...*/
/* USER CODE BEGIN Includes */
#include "Com_Global.h"
#include "Com_Debug.h"
#include "Int_OLED.h"
#include "Int_Key.h"
#include "App_Task.h"
/* USER CODE END Includes */
/* 代码省略 ...*/
int main(void)
{
/* 代码省略 ...*/
/* USER CODE BEGIN 2 */
// 串口输出标题
DEBUG_PRINTLN("Stepper Motor Project");
// 启动 FreeRTOS 任务管理
DEBUG_PRINTLN("启动FreeRTOS任务管理...");
App_Task_Start();
/* USER CODE END 2 */
/* 代码省略 ...*/
}
/* 代码省略 ...*/