Add core firmware modules: input, menu, NRF24L01, OLED, protocol, PWM, storage + UI assets
This commit is contained in:
189
Core/Src/input.c
Normal file
189
Core/Src/input.c
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 按键输入与 ADC 读取模块实现
|
||||
*/
|
||||
#include "input.h"
|
||||
|
||||
/* 全局数据 */
|
||||
uint16_t adc_values[ADC_CHANNEL_COUNT];
|
||||
uint8_t key_values[4];
|
||||
|
||||
/* 按键引脚: PB12, PB13, PB14, PB15 */
|
||||
static const uint16_t key_pins[4] = {GPIO_PIN_12, GPIO_PIN_13, GPIO_PIN_14, GPIO_PIN_15};
|
||||
|
||||
/* 按键去抖状态机 */
|
||||
static uint8_t key_debounce[4]; /* 去抖计数器 */
|
||||
static uint8_t key_current[4]; /* 当前稳定状态 */
|
||||
static uint8_t key_prev[4]; /* 上一帧状态 */
|
||||
static uint32_t key_press_time[4]; /* 按下时刻 (ms) */
|
||||
static uint8_t key_state[4]; /* 状态: IDLE/PRESS/HOLD/RELEASE */
|
||||
static uint32_t sys_tick_ms; /* 系统毫秒计数 */
|
||||
|
||||
/* ADC 通道映射表 (ADC_INx -> ADC2 channel) */
|
||||
static const uint32_t adc_channel_map[ADC_CHANNEL_COUNT] = {
|
||||
ADC_CHANNEL_10, /* ADC_IN0 = PC0 */
|
||||
ADC_CHANNEL_11, /* ADC_IN1 = PC1 */
|
||||
ADC_CHANNEL_12, /* ADC_IN2 = PC2 */
|
||||
ADC_CHANNEL_13, /* ADC_IN3 = PC3 */
|
||||
ADC_CHANNEL_0, /* ADC_IN4 = PA0 */
|
||||
ADC_CHANNEL_1, /* ADC_IN5 = PA1 */
|
||||
ADC_CHANNEL_2, /* ADC_IN6 = PA2 */
|
||||
ADC_CHANNEL_3, /* ADC_IN7 = PA3 */
|
||||
ADC_CHANNEL_4, /* ADC_IN8 = PA4 */
|
||||
ADC_CHANNEL_5, /* ADC_IN9 = PA5 */
|
||||
ADC_CHANNEL_6, /* ADC_IN10 = PA6 */
|
||||
ADC_CHANNEL_7, /* ADC_IN11 = PA7 */
|
||||
ADC_CHANNEL_14, /* ADC_IN12 = PC4 */
|
||||
ADC_CHANNEL_15, /* ADC_IN13 = PC5 */
|
||||
ADC_CHANNEL_8, /* ADC_IN14 = PB0 */
|
||||
ADC_CHANNEL_9, /* ADC_IN15 = PB1 */
|
||||
};
|
||||
|
||||
void Input_Init(void) {
|
||||
sys_tick_ms = 0;
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
key_debounce[i] = 0;
|
||||
key_current[i] = 0;
|
||||
key_prev[i] = 0;
|
||||
key_press_time[i] = 0;
|
||||
key_state[i] = KEY_STATE_IDLE;
|
||||
}
|
||||
for (uint8_t i = 0; i < ADC_CHANNEL_COUNT; i++) {
|
||||
adc_values[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 由 SysTick 中断调用,更新系统时间 */
|
||||
void Input_TickInc(void) {
|
||||
sys_tick_ms++;
|
||||
}
|
||||
|
||||
void Input_Scan(void) {
|
||||
/* ---- 按键扫描 ---- */
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
uint8_t raw = (HAL_GPIO_ReadPin(GPIOB, key_pins[i]) == GPIO_PIN_RESET) ? 1 : 0;
|
||||
|
||||
/* 去抖 */
|
||||
if (raw == key_current[i]) {
|
||||
key_debounce[i] = 0;
|
||||
} else {
|
||||
key_debounce[i]++;
|
||||
if (key_debounce[i] >= 3) { /* 30ms 去抖 */
|
||||
key_current[i] = raw;
|
||||
key_debounce[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 状态机 */
|
||||
key_prev[i] = key_current[i]; /* 保存上一帧 (在更新前) */
|
||||
|
||||
if (key_current[i]) {
|
||||
/* 按键按下 */
|
||||
if (key_state[i] == KEY_STATE_IDLE) {
|
||||
key_state[i] = KEY_STATE_PRESS;
|
||||
key_press_time[i] = sys_tick_ms;
|
||||
} else if (key_state[i] == KEY_STATE_PRESS) {
|
||||
if (sys_tick_ms - key_press_time[i] >= KEY_LONG_PRESS_MS) {
|
||||
key_state[i] = KEY_STATE_HOLD;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 按键释放 */
|
||||
if (key_state[i] == KEY_STATE_PRESS || key_state[i] == KEY_STATE_HOLD) {
|
||||
key_state[i] = KEY_STATE_RELEASE;
|
||||
} else {
|
||||
key_state[i] = KEY_STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
key_values[i] = key_current[i];
|
||||
}
|
||||
|
||||
/* ---- ADC 读取 (轮询单通道) ---- */
|
||||
static uint8_t adc_scan_idx = 0;
|
||||
/* 配置当前通道并读取 */
|
||||
ADC_ChannelConfTypeDef sConfig = {0};
|
||||
sConfig.Channel = adc_channel_map[adc_scan_idx];
|
||||
sConfig.Rank = ADC_REGULAR_RANK_1;
|
||||
sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES_5;
|
||||
HAL_ADC_ConfigChannel(&hadc2, &sConfig);
|
||||
|
||||
HAL_ADC_Start(&hadc2);
|
||||
if (HAL_ADC_PollForConversion(&hadc2, 5) == HAL_OK) {
|
||||
adc_values[adc_scan_idx] = HAL_ADC_GetValue(&hadc2);
|
||||
}
|
||||
HAL_ADC_Stop(&hadc2);
|
||||
|
||||
adc_scan_idx++;
|
||||
if (adc_scan_idx >= ADC_CHANNEL_COUNT) adc_scan_idx = 0;
|
||||
}
|
||||
|
||||
/* ---- 按键查询 ---- */
|
||||
uint8_t Input_GetKey(void) {
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
if (key_state[i] == KEY_STATE_PRESS) {
|
||||
key_state[i] = KEY_STATE_IDLE; /* 消费事件 */
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return KEY_NONE;
|
||||
}
|
||||
|
||||
uint8_t Input_GetKeyState(uint8_t key) {
|
||||
if (key > 3) return KEY_STATE_IDLE;
|
||||
return key_state[key];
|
||||
}
|
||||
|
||||
uint8_t Input_IsKeyPressed(uint8_t key) {
|
||||
if (key > 3) return 0;
|
||||
if (key_state[key] == KEY_STATE_PRESS) {
|
||||
key_state[key] = KEY_STATE_IDLE;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t Input_IsKeyHeld(uint8_t key) {
|
||||
if (key > 3) return 0;
|
||||
return (key_state[key] == KEY_STATE_HOLD) ? 1 : 0;
|
||||
}
|
||||
|
||||
uint8_t Input_IsKeyReleased(uint8_t key) {
|
||||
if (key > 3) return 0;
|
||||
if (key_state[key] == KEY_STATE_RELEASE) {
|
||||
key_state[key] = KEY_STATE_IDLE;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- ADC 查询 ---- */
|
||||
uint16_t Input_GetADC(uint8_t ch) {
|
||||
if (ch >= ADC_CHANNEL_COUNT) return 0;
|
||||
return adc_values[ch];
|
||||
}
|
||||
|
||||
uint16_t Input_GetADC_Percent(uint8_t ch) {
|
||||
if (ch >= ADC_CHANNEL_COUNT) return 0;
|
||||
uint32_t val = adc_values[ch];
|
||||
return (uint16_t)(val * 1000 / ADC_RAW_MAX);
|
||||
}
|
||||
|
||||
void Input_ReadAllADC(void) {
|
||||
/* 已在 Input_Scan 中轮询完成 */
|
||||
}
|
||||
|
||||
/* ---- 引脚编码解码 ---- */
|
||||
GPIO_TypeDef* Config_GetPort(uint16_t pin_code) {
|
||||
uint8_t port = (pin_code >> 8) & 0x0F;
|
||||
switch (port) {
|
||||
case 0: return GPIOA;
|
||||
case 1: return GPIOB;
|
||||
case 2: return GPIOC;
|
||||
case 3: return GPIOD;
|
||||
default: return GPIOA;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t Config_GetPin(uint16_t pin_code) {
|
||||
return (uint16_t)1 << (pin_code & 0x0F);
|
||||
}
|
||||
124
Core/Src/main.c
124
Core/Src/main.c
@@ -2,17 +2,23 @@
|
||||
/**
|
||||
******************************************************************************
|
||||
* @file : main.c
|
||||
* @brief : Main program body
|
||||
* @brief : 萝莉3代航模遥控器 - 主程序
|
||||
* @author : 小云 (Hermes Agent)
|
||||
* @date : 2026-06-25
|
||||
******************************************************************************
|
||||
* @attention
|
||||
*
|
||||
* Copyright (c) 2026 STMicroelectronics.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is licensed under terms that can be found in the LICENSE file
|
||||
* in the root directory of this software component.
|
||||
* If no LICENSE file comes with this software, it is provided AS-IS.
|
||||
* 系统架构:
|
||||
* - 10ms 定时循环: 按键扫描 + ADC 轮询 + LED 更新
|
||||
* - 主循环: 混控计算 + 菜单 UI + 协议收发
|
||||
* - 中断: SysTick (时基), EXTI (NRF IRQ), UART (SBUS)
|
||||
*
|
||||
* 模块依赖:
|
||||
* oled.h -> OLED SSD1306 128x64 显示
|
||||
* input.h -> 按键/ADC 输入采集
|
||||
* menu.h -> 菜单系统与 UI 交互
|
||||
* storage.h -> Flash 存储 (模型/配置)
|
||||
* pwm_out.h -> PWM/LED/WS2812 输出
|
||||
* nrf24l01.h-> NRF24L01+ 2.4G 无线
|
||||
* protocol.h-> 协议层 (RF/SBUS/USB HID)
|
||||
******************************************************************************
|
||||
*/
|
||||
/* USER CODE END Header */
|
||||
@@ -26,6 +32,15 @@
|
||||
#include "usb.h"
|
||||
#include "gpio.h"
|
||||
|
||||
/* 应用层模块 */
|
||||
#include "oled.h"
|
||||
#include "input.h"
|
||||
#include "menu.h"
|
||||
#include "storage.h"
|
||||
#include "pwm_out.h"
|
||||
#include "nrf24l01.h"
|
||||
#include "protocol.h"
|
||||
|
||||
/* Private includes ----------------------------------------------------------*/
|
||||
/* USER CODE BEGIN Includes */
|
||||
|
||||
@@ -38,7 +53,7 @@
|
||||
|
||||
/* Private define ------------------------------------------------------------*/
|
||||
/* USER CODE BEGIN PD */
|
||||
|
||||
#define MAIN_LOOP_PERIOD_MS 10 /* 主循环周期 10ms */
|
||||
/* USER CODE END PD */
|
||||
|
||||
/* Private macro -------------------------------------------------------------*/
|
||||
@@ -49,13 +64,14 @@
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
|
||||
/* USER CODE BEGIN PV */
|
||||
|
||||
static uint32_t last_loop_tick = 0;
|
||||
/* USER CODE END PV */
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
void SystemClock_Config(void);
|
||||
/* USER CODE BEGIN PFP */
|
||||
|
||||
static void System_Init(void);
|
||||
static void System_Loop10ms(void);
|
||||
/* USER CODE END PFP */
|
||||
|
||||
/* Private user code ---------------------------------------------------------*/
|
||||
@@ -69,7 +85,6 @@ void SystemClock_Config(void);
|
||||
*/
|
||||
int main(void)
|
||||
{
|
||||
|
||||
/* USER CODE BEGIN 1 */
|
||||
|
||||
/* USER CODE END 1 */
|
||||
@@ -101,8 +116,10 @@ int main(void)
|
||||
MX_USB_PCD_Init();
|
||||
MX_TIM3_Init();
|
||||
MX_TIM4_Init();
|
||||
/* USER CODE BEGIN 2 */
|
||||
|
||||
/* USER CODE BEGIN 2 */
|
||||
/* ---- 应用层初始化 ---- */
|
||||
System_Init();
|
||||
/* USER CODE END 2 */
|
||||
|
||||
/* Infinite loop */
|
||||
@@ -112,6 +129,18 @@ int main(void)
|
||||
/* USER CODE END WHILE */
|
||||
|
||||
/* USER CODE BEGIN 3 */
|
||||
uint32_t now = HAL_GetTick();
|
||||
|
||||
/* 10ms 定时任务 */
|
||||
if (now - last_loop_tick >= MAIN_LOOP_PERIOD_MS) {
|
||||
last_loop_tick = now;
|
||||
System_Loop10ms();
|
||||
}
|
||||
|
||||
/* 实时任务: 混控 + 菜单 + 协议 */
|
||||
Mixer_Compute(g_channels);
|
||||
Menu_Run();
|
||||
Protocol_Run();
|
||||
}
|
||||
/* USER CODE END 3 */
|
||||
}
|
||||
@@ -165,6 +194,67 @@ void SystemClock_Config(void)
|
||||
|
||||
/* USER CODE BEGIN 4 */
|
||||
|
||||
/**
|
||||
* @brief 系统初始化: 加载配置、初始化各模块、显示开机画面
|
||||
* @retval None
|
||||
*/
|
||||
static void System_Init(void) {
|
||||
/* 1. 加载存储配置 */
|
||||
Storage_Init();
|
||||
Storage_Load();
|
||||
|
||||
/* 2. 初始化 OLED 并显示开机画面 */
|
||||
OLED_Init();
|
||||
OLED_DrawBitmap(0, 0, bitmap_bytes, 64, 64); /* 开机 Logo */
|
||||
OLED_ShowString(70, 20, "Loli3", 16);
|
||||
OLED_ShowString(70, 40, "RC v3.0", 6);
|
||||
OLED_Display();
|
||||
HAL_Delay(2000); /* 显示 2 秒 */
|
||||
|
||||
/* 3. 初始化输入模块 */
|
||||
Input_Init();
|
||||
|
||||
/* 4. 初始化 PWM/LED */
|
||||
PWM_Init();
|
||||
LED_SetSystem(1); /* 系统 LED 常亮 */
|
||||
|
||||
/* 5. 初始化 NRF24L01 */
|
||||
NRF24L01_Init();
|
||||
if (NRF24L01_Check()) {
|
||||
/* NRF24L01 存在 */
|
||||
NRF24L01_SetChannel(g_config.nrf_channel);
|
||||
LED_SetNRF(0); /* 慢闪 = 未连接 */
|
||||
} else {
|
||||
/* NRF24L01 未检测到 */
|
||||
LED_SetNRF(3); /* 灭 */
|
||||
}
|
||||
|
||||
/* 6. 初始化协议层 */
|
||||
Protocol_Init();
|
||||
|
||||
/* 7. 初始化菜单 */
|
||||
Menu_Init();
|
||||
|
||||
/* 8. 根据配置设置 PWM 模式 */
|
||||
PWM_SetMode(0, g_config.pwm1_mode);
|
||||
PWM_SetMode(1, g_config.pwm2_mode);
|
||||
PWM_SetWS2812Mode(g_config.ws2812_effect);
|
||||
PWM_SetLEDBrightness(g_config.led_brightness);
|
||||
PWM_SetVibrateLevel(g_config.vibrate_level);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 10ms 定时任务: 按键扫描 + ADC 轮询 + LED 闪烁更新
|
||||
* @retval None
|
||||
*/
|
||||
static void System_Loop10ms(void) {
|
||||
/* 按键 + ADC 扫描 */
|
||||
Input_Scan();
|
||||
|
||||
/* LED 闪烁更新 */
|
||||
LED_Update();
|
||||
}
|
||||
|
||||
/* USER CODE END 4 */
|
||||
|
||||
/**
|
||||
@@ -174,13 +264,17 @@ void SystemClock_Config(void)
|
||||
void Error_Handler(void)
|
||||
{
|
||||
/* USER CODE BEGIN Error_Handler_Debug */
|
||||
/* User can add his own implementation to report the HAL error return state */
|
||||
/* 错误时 LED 快闪 */
|
||||
__disable_irq();
|
||||
while (1)
|
||||
{
|
||||
/* 系统 LED 快闪指示错误 */
|
||||
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_6);
|
||||
for (volatile uint32_t i = 0; i < 500000; i++);
|
||||
}
|
||||
/* USER CODE END Error_Handler_Debug */
|
||||
}
|
||||
|
||||
#ifdef USE_FULL_ASSERT
|
||||
/**
|
||||
* @brief Reports the name of the source file and the source line number
|
||||
|
||||
738
Core/Src/menu.c
Normal file
738
Core/Src/menu.c
Normal file
@@ -0,0 +1,738 @@
|
||||
/**
|
||||
* 菜单系统实现
|
||||
* 基于 OLED 128x64 的层级菜单 UI
|
||||
*/
|
||||
#include "menu.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
/* 全局页面状态 */
|
||||
Page_t g_page;
|
||||
|
||||
/* 通道输出值 (混控后) */
|
||||
int16_t g_channels[CHANNEL_COUNT];
|
||||
|
||||
/* 模型类型名称 */
|
||||
static const char *model_types[] = {"Airplane", "Car", "Boat", "MultiRotor"};
|
||||
|
||||
/* 输入名称缓存 */
|
||||
static char input_name_buf[16];
|
||||
|
||||
/* 获取输入名称 */
|
||||
static const char* GetInputName(uint8_t idx, uint8_t type) {
|
||||
if (type == INPUT_TYPE_ADC) {
|
||||
snprintf(input_name_buf, sizeof(input_name_buf), "ADC_IN%d", idx);
|
||||
} else if (type == INPUT_TYPE_KEY) {
|
||||
snprintf(input_name_buf, sizeof(input_name_buf), "KEY_IN%d", idx);
|
||||
} else {
|
||||
snprintf(input_name_buf, sizeof(input_name_buf), "None");
|
||||
}
|
||||
return input_name_buf;
|
||||
}
|
||||
|
||||
/* 获取通道值显示 */
|
||||
static const char* GetChannelValue(uint8_t ch) {
|
||||
Model_t *m = Storage_GetActiveModel();
|
||||
if (m->input_type[ch] == INPUT_TYPE_ADC) {
|
||||
snprintf(input_name_buf, sizeof(input_name_buf), "%4d",
|
||||
adc_values[m->channel_map[ch]]);
|
||||
} else if (m->input_type[ch] == INPUT_TYPE_KEY) {
|
||||
snprintf(input_name_buf, sizeof(input_name_buf), "%s",
|
||||
key_values[m->channel_map[ch]] ? "ON " : "OFF");
|
||||
} else {
|
||||
snprintf(input_name_buf, sizeof(input_name_buf), "----");
|
||||
}
|
||||
return input_name_buf;
|
||||
}
|
||||
|
||||
void Menu_Init(void) {
|
||||
memset(&g_page, 0, sizeof(Page_t));
|
||||
g_page.page_id = PAGE_HOME;
|
||||
g_page.cursor = 0;
|
||||
}
|
||||
|
||||
/* ---- 混控计算 ---- */
|
||||
void Mixer_Compute(int16_t *channels) {
|
||||
Model_t *m = Storage_GetActiveModel();
|
||||
|
||||
for (uint8_t i = 0; i < CHANNEL_COUNT; i++) {
|
||||
if (m->input_type[i] == INPUT_TYPE_NONE) {
|
||||
channels[i] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
int16_t raw = 0;
|
||||
if (m->input_type[i] == INPUT_TYPE_ADC) {
|
||||
/* ADC: 0-4095 -> -1000 ~ +1000 */
|
||||
raw = (int16_t)(adc_values[m->channel_map[i]]) - 2048;
|
||||
raw = raw * 1000 / 2048;
|
||||
} else {
|
||||
/* 按键: 0/1 -> -1000/+1000 */
|
||||
raw = key_values[m->channel_map[i]] ? 1000 : -1000;
|
||||
}
|
||||
|
||||
/* 应用混控 */
|
||||
switch (m->mixer_type[i]) {
|
||||
case MIXER_TYPE_SIMPLE:
|
||||
channels[i] = (raw * m->mixer_scale[i] / 100) + m->mixer_offset[i];
|
||||
break;
|
||||
case MIXER_TYPE_DELTA:
|
||||
/* 三角翼: CH1=elevon_left, CH2=elevon_right */
|
||||
if (i == 0) channels[i] = (raw * m->mixer_scale[i] / 100) + m->mixer_offset[i];
|
||||
if (i == 1) channels[i] = (-raw * m->mixer_scale[i] / 100) + m->mixer_offset[i];
|
||||
break;
|
||||
case MIXER_TYPE_VTAIL:
|
||||
channels[i] = (raw * m->mixer_scale[i] / 100) + m->mixer_offset[i];
|
||||
break;
|
||||
case MIXER_TYPE_ELEVON:
|
||||
channels[i] = (raw * m->mixer_scale[i] / 100) + m->mixer_offset[i];
|
||||
break;
|
||||
default:
|
||||
channels[i] = raw;
|
||||
break;
|
||||
}
|
||||
|
||||
/* 限幅 */
|
||||
if (channels[i] > 1000) channels[i] = 1000;
|
||||
if (channels[i] < -1000) channels[i] = -1000;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 通用菜单导航 ---- */
|
||||
static void Menu_Navigate(void) {
|
||||
uint8_t key = Input_GetKey();
|
||||
|
||||
if (key == KEY_UP) {
|
||||
if (g_page.cursor > 0) g_page.cursor--;
|
||||
} else if (key == KEY_DOWN) {
|
||||
if (g_page.cursor < g_page.item_count - 1) g_page.cursor++;
|
||||
} else if (key == KEY_OK) {
|
||||
/* 执行选中项 */
|
||||
if (g_page.cursor < g_page.item_count && g_page.items[g_page.cursor].target_page != 0xFF) {
|
||||
g_page.page_id = g_page.items[g_page.cursor].target_page;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
if (g_page.cursor < g_page.item_count && g_page.items[g_page.cursor].action) {
|
||||
g_page.items[g_page.cursor].action();
|
||||
}
|
||||
} else if (key == KEY_BACK) {
|
||||
/* 返回上一级 */
|
||||
if (g_page.page_id == PAGE_HOME) {
|
||||
/* 首页按返回: 切换值/回传 */
|
||||
g_config.home_page = !g_config.home_page;
|
||||
} else if (g_page.page_id == PAGE_MAIN_MENU) {
|
||||
g_page.page_id = PAGE_HOME;
|
||||
} else if (g_page.page_id >= PAGE_MODEL_LIST && g_page.page_id <= PAGE_MIXER_EDIT) {
|
||||
g_page.page_id = PAGE_MAIN_MENU;
|
||||
} else {
|
||||
g_page.page_id = PAGE_MAIN_MENU;
|
||||
}
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 首页 ---- */
|
||||
void Menu_DrawHome(void) {
|
||||
OLED_Clear();
|
||||
|
||||
if (g_config.home_page == 0) {
|
||||
/* 回传信息页 */
|
||||
OLED_ShowString(0, 0, "Telemetry", 6);
|
||||
OLED_DrawLine(0, 9, 127, 9);
|
||||
|
||||
/* 电压 */
|
||||
OLED_ShowString(0, 14, "V:", 6);
|
||||
OLED_ShowNum(18, 14, 0, 4, 6); /* TODO: 实际电压 */
|
||||
OLED_ShowString(48, 14, "V", 6);
|
||||
|
||||
/* RSSI */
|
||||
OLED_ShowString(0, 24, "RSSI:", 6);
|
||||
OLED_ShowNum(36, 24, 0, 3, 6);
|
||||
|
||||
/* 丢包率 */
|
||||
OLED_ShowString(0, 34, "Lost:", 6);
|
||||
OLED_ShowNum(36, 34, 0, 3, 6);
|
||||
OLED_ShowString(60, 34, "%", 6);
|
||||
|
||||
/* 模型名 */
|
||||
Model_t *m = Storage_GetActiveModel();
|
||||
OLED_ShowString(0, 48, m->name, 6);
|
||||
|
||||
/* 高频头状态 */
|
||||
OLED_ShowString(80, 48, g_config.rf_type == 0 ? "NRF" : "CRFS", 6);
|
||||
OLED_ShowString(80, 56, nrf_connected ? "OK" : "--", 6);
|
||||
} else {
|
||||
/* INx 值页 */
|
||||
OLED_ShowString(0, 0, "ADC Values", 6);
|
||||
OLED_DrawLine(0, 9, 127, 9);
|
||||
|
||||
Model_t *m = Storage_GetActiveModel();
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
uint8_t row = i / 2;
|
||||
uint8_t col = (i % 2) * 64;
|
||||
OLED_ShowString(col, 12 + row * 12, "IN", 6);
|
||||
OLED_ShowNum(col + 12, 12 + row * 12, i, 2, 6);
|
||||
OLED_ShowString(col + 24, 12 + row * 12, ":", 6);
|
||||
if (m->input_type[i] == INPUT_TYPE_ADC) {
|
||||
OLED_ShowNum(col + 30, 12 + row * 12,
|
||||
adc_values[m->channel_map[i]], 4, 6);
|
||||
} else {
|
||||
OLED_ShowString(col + 30, 12 + row * 12,
|
||||
key_values[m->channel_map[i]] ? "ON " : "OFF", 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
/* 首页长按确认 -> 高频头快捷页 */
|
||||
if (Input_IsKeyHeld(KEY_OK)) {
|
||||
g_page.page_id = PAGE_RF_QUICK;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
|
||||
/* 长按返回 2s -> 休眠 */
|
||||
if (Input_IsKeyHeld(KEY_BACK)) {
|
||||
/* TODO: 进入休眠模式 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 主菜单 ---- */
|
||||
void Menu_DrawMainMenu(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Main Menu", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
static const char *items[] = {
|
||||
"Model Select",
|
||||
"RF Config",
|
||||
"Channel Map",
|
||||
"Mixer",
|
||||
"PWM Driver",
|
||||
"Home Page"
|
||||
};
|
||||
g_page.item_count = 6;
|
||||
|
||||
for (uint8_t i = 0; i < g_page.item_count; i++) {
|
||||
uint8_t y = 20 + i * 10;
|
||||
if (i == g_page.cursor) {
|
||||
OLED_ShowString(0, y, ">", 6);
|
||||
}
|
||||
OLED_ShowString(8, y, items[i], 6);
|
||||
}
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
/* 映射到具体页面 */
|
||||
if (Input_IsKeyPressed(KEY_OK)) {
|
||||
switch (g_page.cursor) {
|
||||
case 0: g_page.page_id = PAGE_MODEL_LIST; break;
|
||||
case 1: g_page.page_id = PAGE_RF_CONFIG; break;
|
||||
case 2: g_page.page_id = PAGE_CHANNEL_MAP; break;
|
||||
case 3: g_page.page_id = PAGE_MIXER; break;
|
||||
case 4: g_page.page_id = PAGE_PWM_CONFIG; break;
|
||||
case 5: g_page.page_id = PAGE_HOME_SET; break;
|
||||
}
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 模型列表 ---- */
|
||||
void Menu_DrawModelList(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Models", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 0;
|
||||
/* 第一项: 新建 */
|
||||
OLED_ShowString(0, 20, g_page.cursor == 0 ? ">[New]" : " [New]", 6);
|
||||
g_page.item_count++;
|
||||
|
||||
for (uint8_t i = 0; i < MODEL_MAX_COUNT; i++) {
|
||||
if (g_config.models[i].name[0] != '\0') {
|
||||
uint8_t y = 20 + g_page.item_count * 10;
|
||||
if (y > 54) break; /* 超出屏幕 */
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "%s[%c] %s",
|
||||
g_page.cursor == g_page.item_count ? ">" : " ",
|
||||
i == g_config.active_model ? '*' : ' ',
|
||||
g_config.models[i].name);
|
||||
OLED_ShowString(0, y, buf, 6);
|
||||
g_page.item_count++;
|
||||
}
|
||||
}
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
/* 处理选择 */
|
||||
if (Input_IsKeyPressed(KEY_OK)) {
|
||||
if (g_page.cursor == 0) {
|
||||
/* 新建模型 */
|
||||
g_page.page_id = PAGE_MODEL_NEW;
|
||||
g_page.cursor = 0;
|
||||
g_page.edit_idx = 0;
|
||||
g_page.edit_str_len = 0;
|
||||
memset(g_page.edit_str, 0, sizeof(g_page.edit_str));
|
||||
} else {
|
||||
/* 选择模型 */
|
||||
uint8_t model_idx = 0xFF;
|
||||
uint8_t cnt = 1;
|
||||
for (uint8_t i = 0; i < MODEL_MAX_COUNT; i++) {
|
||||
if (g_config.models[i].name[0] != '\0') {
|
||||
if (cnt == g_page.cursor) { model_idx = i; break; }
|
||||
cnt++;
|
||||
}
|
||||
}
|
||||
if (model_idx != 0xFF) {
|
||||
Storage_ModelSelect(model_idx);
|
||||
g_page.page_id = PAGE_MAIN_MENU;
|
||||
}
|
||||
}
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
|
||||
/* 长按确认 -> 编辑/删除 */
|
||||
if (Input_IsKeyHeld(KEY_OK) && g_page.cursor > 0) {
|
||||
/* TODO: 弹出编辑/删除选项 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 新建模型 ---- */
|
||||
void Menu_DrawModelNew(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "New Model", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
/* 名称编辑 */
|
||||
OLED_ShowString(0, 22, "Name:", 6);
|
||||
OLED_ShowString(36, 22, g_page.edit_str, 6);
|
||||
if (g_page.edit_idx == 0) {
|
||||
/* 闪烁光标 */
|
||||
OLED_ShowString(36 + g_page.edit_str_len * 6, 22, "_", 6);
|
||||
}
|
||||
|
||||
/* 类型选择 */
|
||||
OLED_ShowString(0, 34, "Type:", 6);
|
||||
OLED_ShowString(36, 34, (char*)model_types[g_page.edit_value], 6);
|
||||
|
||||
/* 按钮 */
|
||||
OLED_ShowString(20, 50, "[OK]", 6);
|
||||
OLED_ShowString(80, 50, "[Cancel]", 6);
|
||||
|
||||
OLED_Display();
|
||||
|
||||
/* 处理输入 */
|
||||
uint8_t key = Input_GetKey();
|
||||
if (g_page.edit_idx == 0) {
|
||||
/* 编辑名称 */
|
||||
if (key == KEY_UP) {
|
||||
/* 字符递增 */
|
||||
if (g_page.edit_str_len < 8) {
|
||||
g_page.edit_str[g_page.edit_str_len] = 'A';
|
||||
g_page.edit_str_len++;
|
||||
}
|
||||
} else if (key == KEY_DOWN) {
|
||||
if (g_page.edit_str_len > 0) {
|
||||
g_page.edit_str_len--;
|
||||
g_page.edit_str[g_page.edit_str_len] = '\0';
|
||||
}
|
||||
} else if (key == KEY_OK) {
|
||||
g_page.edit_idx = 1; /* 切换到类型选择 */
|
||||
}
|
||||
} else if (g_page.edit_idx == 1) {
|
||||
if (key == KEY_UP && g_page.edit_value < 3) g_page.edit_value++;
|
||||
if (key == KEY_DOWN && g_page.edit_value > 0) g_page.edit_value--;
|
||||
if (key == KEY_OK) g_page.edit_idx = 2; /* 到确认按钮 */
|
||||
} else if (g_page.edit_idx == 2) {
|
||||
/* 确认/取消 */
|
||||
if (key == KEY_UP || key == KEY_DOWN) g_page.edit_idx = (g_page.edit_idx == 2) ? 3 : 2;
|
||||
if (key == KEY_OK) {
|
||||
if (g_page.edit_idx == 2) {
|
||||
/* 确认创建 */
|
||||
Storage_ModelAdd(g_page.edit_str, g_page.edit_value);
|
||||
g_page.page_id = PAGE_MODEL_LIST;
|
||||
} else {
|
||||
g_page.page_id = PAGE_MODEL_LIST;
|
||||
}
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (key == KEY_BACK) {
|
||||
g_page.page_id = PAGE_MODEL_LIST;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 通道映射 ---- */
|
||||
void Menu_DrawChannelMap(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Channel Map", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
Model_t *m = Storage_GetActiveModel();
|
||||
g_page.item_count = CHANNEL_COUNT + 1; /* +1 for [Next] */
|
||||
|
||||
uint8_t start = g_page.scroll_offset;
|
||||
for (uint8_t i = start; i < start + 4 && i < CHANNEL_COUNT; i++) {
|
||||
uint8_t y = 20 + (i - start) * 10;
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%s[CH%d]->[%s]",
|
||||
i == g_page.cursor ? ">" : " ",
|
||||
i + 1,
|
||||
GetInputName(m->channel_map[i], m->input_type[i]));
|
||||
OLED_ShowString(0, y, buf, 6);
|
||||
}
|
||||
|
||||
/* [下一步] 按钮 */
|
||||
if (g_page.cursor >= CHANNEL_COUNT - 4) {
|
||||
OLED_ShowString(40, 56, "[Next]", 6);
|
||||
}
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
/* 编辑通道映射 */
|
||||
if (Input_IsKeyPressed(KEY_OK) && g_page.cursor < CHANNEL_COUNT) {
|
||||
/* 循环切换输入源 */
|
||||
m->input_type[g_page.cursor]++;
|
||||
if (m->input_type[g_page.cursor] > INPUT_TYPE_KEY)
|
||||
m->input_type[g_page.cursor] = INPUT_TYPE_ADC;
|
||||
}
|
||||
|
||||
/* 下一步 */
|
||||
if (Input_IsKeyPressed(KEY_OK) && g_page.cursor == CHANNEL_COUNT) {
|
||||
g_page.page_id = PAGE_MIXER;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 混控列表 ---- */
|
||||
void Menu_DrawMixer(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Mixer", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = CHANNEL_COUNT + 1;
|
||||
|
||||
uint8_t start = g_page.scroll_offset;
|
||||
for (uint8_t i = start; i < start + 4 && i < CHANNEL_COUNT; i++) {
|
||||
uint8_t y = 20 + (i - start) * 10;
|
||||
char buf[20];
|
||||
snprintf(buf, sizeof(buf), "%s[CH%d]: %4d",
|
||||
i == g_page.cursor ? ">" : " ",
|
||||
i + 1, g_channels[i]);
|
||||
OLED_ShowString(0, y, buf, 6);
|
||||
}
|
||||
|
||||
if (g_page.cursor >= CHANNEL_COUNT - 4) {
|
||||
OLED_ShowString(40, 56, "[Next]", 6);
|
||||
}
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
if (Input_IsKeyPressed(KEY_OK) && g_page.cursor < CHANNEL_COUNT) {
|
||||
g_page.page_id = PAGE_MIXER_EDIT;
|
||||
g_page.edit_idx = g_page.cursor;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 混控编辑 ---- */
|
||||
void Menu_DrawMixerEdit(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Mixer Edit", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
Model_t *m = Storage_GetActiveModel();
|
||||
uint8_t ch = g_page.edit_idx;
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "CH%d Mixer", ch + 1);
|
||||
OLED_ShowString(0, 20, buf, 6);
|
||||
|
||||
/* 混控类型 */
|
||||
static const char *mixer_names[] = {"None", "Simple", "Delta", "V-Tail", "Elevon"};
|
||||
snprintf(buf, sizeof(buf), "Type: %s", mixer_names[m->mixer_type[ch]]);
|
||||
OLED_ShowString(0, 30, buf, 6);
|
||||
|
||||
/* 比例 */
|
||||
snprintf(buf, sizeof(buf), "Scale: %d%%", m->mixer_scale[ch]);
|
||||
OLED_ShowString(0, 40, buf, 6);
|
||||
|
||||
/* 偏移 */
|
||||
snprintf(buf, sizeof(buf), "Offset: %d", m->mixer_offset[ch]);
|
||||
OLED_ShowString(0, 50, buf, 6);
|
||||
|
||||
OLED_Display();
|
||||
|
||||
uint8_t key = Input_GetKey();
|
||||
if (key == KEY_UP) {
|
||||
if (g_page.cursor == 0) {
|
||||
m->mixer_type[ch]++;
|
||||
if (m->mixer_type[ch] > MIXER_TYPE_ELEVON) m->mixer_type[ch] = MIXER_TYPE_NONE;
|
||||
} else if (g_page.cursor == 1) {
|
||||
m->mixer_scale[ch] += 5;
|
||||
if (m->mixer_scale[ch] > 200) m->mixer_scale[ch] = 200;
|
||||
} else if (g_page.cursor == 2) {
|
||||
m->mixer_offset[ch] += 10;
|
||||
}
|
||||
} else if (key == KEY_DOWN) {
|
||||
if (g_page.cursor == 0) {
|
||||
if (m->mixer_type[ch] > 0) m->mixer_type[ch]--;
|
||||
} else if (g_page.cursor == 1) {
|
||||
m->mixer_scale[ch] -= 5;
|
||||
if (m->mixer_scale[ch] < 0) m->mixer_scale[ch] = 0;
|
||||
} else if (g_page.cursor == 2) {
|
||||
m->mixer_offset[ch] -= 10;
|
||||
}
|
||||
} else if (key == KEY_OK) {
|
||||
g_page.cursor++;
|
||||
if (g_page.cursor > 2) g_page.cursor = 0;
|
||||
} else if (key == KEY_BACK) {
|
||||
g_page.page_id = PAGE_MIXER;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 高频头配置 ---- */
|
||||
void Menu_DrawRFConfig(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "RF Config", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 2;
|
||||
|
||||
OLED_ShowString(0, 22, g_page.cursor == 0 ? ">[NRF24]" : " [NRF24]", 6);
|
||||
OLED_ShowString(0, 34, g_page.cursor == 1 ? ">[CRFS]" : " [CRFS]", 6);
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
if (Input_IsKeyPressed(KEY_OK)) {
|
||||
if (g_page.cursor == 0) {
|
||||
g_config.rf_type = 0;
|
||||
g_page.page_id = PAGE_NRF_CONFIG;
|
||||
} else {
|
||||
g_config.rf_type = 1;
|
||||
g_page.page_id = PAGE_CRFS_CONFIG;
|
||||
}
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- NRF24 配置 ---- */
|
||||
void Menu_DrawNRFConfig(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "NRF24 Config", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 5;
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%sPhrase: %s",
|
||||
g_page.cursor == 0 ? ">" : " ", g_config.nrf_phrase);
|
||||
OLED_ShowString(0, 20, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sChannel: %d",
|
||||
g_page.cursor == 1 ? ">" : " ", g_config.nrf_channel);
|
||||
OLED_ShowString(0, 30, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sAddr: %02X%02X%02X%02X%02X",
|
||||
g_page.cursor == 2 ? ">" : " ",
|
||||
g_config.nrf_addr[0], g_config.nrf_addr[1],
|
||||
g_config.nrf_addr[2], g_config.nrf_addr[3],
|
||||
g_config.nrf_addr[4]);
|
||||
OLED_ShowString(0, 40, buf, 6);
|
||||
|
||||
static const char *rates[] = {"250K", "1M", "2M"};
|
||||
snprintf(buf, sizeof(buf), "%sRate: %s",
|
||||
g_page.cursor == 3 ? ">" : " ", rates[g_config.nrf_rate]);
|
||||
OLED_ShowString(0, 50, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sPower: %d",
|
||||
g_page.cursor == 4 ? ">" : " ", g_config.nrf_power);
|
||||
OLED_ShowString(0, 60, buf, 6);
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
}
|
||||
|
||||
/* ---- CRFS 配置 ---- */
|
||||
void Menu_DrawCRFSConfig(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "CRFS Config", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 3;
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%sPkt Rate: %dHz",
|
||||
g_page.cursor == 0 ? ">" : " ", g_config.crfs_packet_rate);
|
||||
OLED_ShowString(0, 22, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sTelem: 1:%d",
|
||||
g_page.cursor == 1 ? ">" : " ", g_config.crfs_telemetry_ratio);
|
||||
OLED_ShowString(0, 34, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sPower: %dmW",
|
||||
g_page.cursor == 2 ? ">" : " ", g_config.crfs_power);
|
||||
OLED_ShowString(0, 46, buf, 6);
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
}
|
||||
|
||||
/* ---- PWM 配置 ---- */
|
||||
void Menu_DrawPWMConfig(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "PWM Config", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 5;
|
||||
|
||||
static const char *pwm_modes[] = {"Disabled", "WS2812", "LED", "Vibrate"};
|
||||
char buf[32];
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sPWM1: %s",
|
||||
g_page.cursor == 0 ? ">" : " ", pwm_modes[g_config.pwm1_mode]);
|
||||
OLED_ShowString(0, 20, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sPWM2: %s",
|
||||
g_page.cursor == 1 ? ">" : " ", pwm_modes[g_config.pwm2_mode]);
|
||||
OLED_ShowString(0, 30, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sWS2812 Effect: %d",
|
||||
g_page.cursor == 2 ? ">" : " ", g_config.ws2812_effect);
|
||||
OLED_ShowString(0, 40, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sLED Bright: %d%%",
|
||||
g_page.cursor == 3 ? ">" : " ", g_config.led_brightness);
|
||||
OLED_ShowString(0, 50, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sVibrate: Lv%d",
|
||||
g_page.cursor == 4 ? ">" : " ", g_config.vibrate_level);
|
||||
OLED_ShowString(0, 60, buf, 6);
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
}
|
||||
|
||||
/* ---- 首页设置 ---- */
|
||||
void Menu_DrawHomeSet(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Home Page", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 2;
|
||||
|
||||
OLED_ShowString(0, 25, g_page.cursor == 0 ? ">Telemetry" : " Telemetry", 6);
|
||||
OLED_ShowString(0, 37, g_page.cursor == 1 ? ">INx Values" : " INx Values", 6);
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
if (Input_IsKeyPressed(KEY_OK)) {
|
||||
g_config.home_page = g_page.cursor;
|
||||
g_page.page_id = PAGE_MAIN_MENU;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 对频页 ---- */
|
||||
void Menu_DrawBind(void) {
|
||||
OLED_Clear();
|
||||
OLED_ShowString(0, 0, "Binding...", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
OLED_ShowString(0, 30, "Scanning channels", 6);
|
||||
OLED_DrawProgressBar(10, 45, 108, 8, 50); /* 示例进度 */
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
}
|
||||
|
||||
/* ---- 高频头快捷页 ---- */
|
||||
void Menu_DrawRFQuick(void) {
|
||||
OLED_Clear();
|
||||
|
||||
if (g_config.rf_type == 0) {
|
||||
OLED_ShowString(0, 0, "NRF24 Quick", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
|
||||
g_page.item_count = 3;
|
||||
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "%s[Bind]",
|
||||
g_page.cursor == 0 ? ">" : " ");
|
||||
OLED_ShowString(0, 22, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sCh: %d",
|
||||
g_page.cursor == 1 ? ">" : " ", g_config.nrf_channel);
|
||||
OLED_ShowString(0, 34, buf, 6);
|
||||
|
||||
snprintf(buf, sizeof(buf), "%sAddr: %02X%02X%02X%02X%02X",
|
||||
g_page.cursor == 2 ? ">" : " ",
|
||||
g_config.nrf_addr[0], g_config.nrf_addr[1],
|
||||
g_config.nrf_addr[2], g_config.nrf_addr[3],
|
||||
g_config.nrf_addr[4]);
|
||||
OLED_ShowString(0, 46, buf, 6);
|
||||
} else {
|
||||
OLED_ShowString(0, 0, "CRFS Quick", 16);
|
||||
OLED_DrawLine(0, 17, 127, 17);
|
||||
OLED_ShowString(0, 30, "CRFS mode active", 6);
|
||||
}
|
||||
|
||||
OLED_Display();
|
||||
Menu_Navigate();
|
||||
|
||||
if (Input_IsKeyPressed(KEY_OK) && g_page.cursor == 0) {
|
||||
/* 进入对频模式 */
|
||||
g_page.page_id = PAGE_BIND;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
|
||||
if (Input_IsKeyPressed(KEY_BACK)) {
|
||||
g_page.page_id = PAGE_HOME;
|
||||
g_page.cursor = 0;
|
||||
g_page.item_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 主运行循环 ---- */
|
||||
void Menu_Run(void) {
|
||||
switch (g_page.page_id) {
|
||||
case PAGE_HOME: Menu_DrawHome(); break;
|
||||
case PAGE_MAIN_MENU: Menu_DrawMainMenu(); break;
|
||||
case PAGE_MODEL_LIST: Menu_DrawModelList(); break;
|
||||
case PAGE_MODEL_NEW: Menu_DrawModelNew(); break;
|
||||
case PAGE_CHANNEL_MAP: Menu_DrawChannelMap(); break;
|
||||
case PAGE_MIXER: Menu_DrawMixer(); break;
|
||||
case PAGE_MIXER_EDIT: Menu_DrawMixerEdit(); break;
|
||||
case PAGE_RF_CONFIG: Menu_DrawRFConfig(); break;
|
||||
case PAGE_NRF_CONFIG: Menu_DrawNRFConfig(); break;
|
||||
case PAGE_CRFS_CONFIG: Menu_DrawCRFSConfig(); break;
|
||||
case PAGE_PWM_CONFIG: Menu_DrawPWMConfig(); break;
|
||||
case PAGE_HOME_SET: Menu_DrawHomeSet(); break;
|
||||
case PAGE_BIND: Menu_DrawBind(); break;
|
||||
case PAGE_RF_QUICK: Menu_DrawRFQuick(); break;
|
||||
default: g_page.page_id = PAGE_HOME; break;
|
||||
}
|
||||
}
|
||||
246
Core/Src/nrf24l01.c
Normal file
246
Core/Src/nrf24l01.c
Normal file
@@ -0,0 +1,246 @@
|
||||
/**
|
||||
* NRF24L01+ 驱动实现
|
||||
*/
|
||||
#include "nrf24l01.h"
|
||||
#include <string.h>
|
||||
|
||||
/* 全局状态 */
|
||||
uint8_t nrf_connected = 0;
|
||||
uint8_t nrf_bind_phrase[7] = "LOVE";
|
||||
uint8_t nrf_channel = 40;
|
||||
uint8_t nrf_tx_addr[NRF_ADDR_WIDTH] = {0xE7, 0xE7, 0xE7, 0xE7, 0xE7};
|
||||
|
||||
/* ---- 低层 SPI 操作 ---- */
|
||||
static void NRF_SPI_CS(uint8_t level) {
|
||||
if (level) NRF_CSN_H(); else NRF_CSN_L();
|
||||
}
|
||||
|
||||
static uint8_t NRF_SPI_Transfer(uint8_t dat) {
|
||||
uint8_t rx;
|
||||
HAL_SPI_TransmitReceive(&hspi1, &dat, &rx, 1, 10);
|
||||
return rx;
|
||||
}
|
||||
|
||||
uint8_t NRF24L01_ReadReg(uint8_t reg) {
|
||||
uint8_t val;
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_R_REGISTER | reg);
|
||||
val = NRF_SPI_Transfer(0xFF);
|
||||
NRF_SPI_CS(1);
|
||||
return val;
|
||||
}
|
||||
|
||||
void NRF24L01_WriteReg(uint8_t reg, uint8_t value) {
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_W_REGISTER | reg);
|
||||
NRF_SPI_Transfer(value);
|
||||
NRF_SPI_CS(1);
|
||||
}
|
||||
|
||||
void NRF24L01_ReadBuf(uint8_t reg, uint8_t *buf, uint8_t len) {
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_R_REGISTER | reg);
|
||||
for (uint8_t i = 0; i < len; i++)
|
||||
buf[i] = NRF_SPI_Transfer(0xFF);
|
||||
NRF_SPI_CS(1);
|
||||
}
|
||||
|
||||
void NRF24L01_WriteBuf(uint8_t reg, const uint8_t *buf, uint8_t len) {
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_W_REGISTER | reg);
|
||||
for (uint8_t i = 0; i < len; i++)
|
||||
NRF_SPI_Transfer(buf[i]);
|
||||
NRF_SPI_CS(1);
|
||||
}
|
||||
|
||||
/* ---- 初始化 ---- */
|
||||
void NRF24L01_Init(void) {
|
||||
NRF_CE_L();
|
||||
NRF_CSN_H();
|
||||
|
||||
/* 上电延时 */
|
||||
HAL_Delay(100);
|
||||
|
||||
/* 配置寄存器 */
|
||||
NRF24L01_WriteReg(NRF_CONFIG, 0x0E); /* EN_CRC, CRC0, PWR_UP, PTX */
|
||||
NRF24L01_WriteReg(NRF_EN_AA, 0x01); /* pipe0 auto ACK */
|
||||
NRF24L01_WriteReg(NRF_EN_RXADDR, 0x01); /* pipe0 enable */
|
||||
NRF24L01_WriteReg(NRF_SETUP_AW, 0x03); /* 5 bytes address */
|
||||
NRF24L01_WriteReg(NRF_SETUP_RETR, 0x1A); /* 500us + 15 retries */
|
||||
NRF24L01_WriteReg(NRF_RF_CH, nrf_channel);
|
||||
NRF24L01_WriteReg(NRF_RF_SETUP, NRF_RATE_1M | NRF_PA_MAX);
|
||||
NRF24L01_WriteReg(NRF_RX_PW_P0, 32); /* payload = 32 bytes */
|
||||
NRF24L01_WriteReg(NRF_FIFO_STATUS, 0x11); /* clear FIFO */
|
||||
|
||||
/* 设置地址 */
|
||||
NRF24L01_SetTXAddr(nrf_tx_addr);
|
||||
NRF24L01_SetRXAddr(0, nrf_tx_addr);
|
||||
|
||||
/* 清空 FIFO */
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_FLUSH_TX);
|
||||
NRF_SPI_CS(1);
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_FLUSH_RX);
|
||||
NRF_SPI_CS(1);
|
||||
|
||||
/* 清除状态 */
|
||||
NRF24L01_WriteReg(NRF_STATUS, NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
|
||||
|
||||
NRF_CE_L();
|
||||
}
|
||||
|
||||
uint8_t NRF24L01_Check(void) {
|
||||
uint8_t buf[5] = {0xA5, 0xA5, 0xA5, 0xA5, 0xA5};
|
||||
uint8_t rx[5];
|
||||
NRF24L01_WriteBuf(NRF_TX_ADDR, buf, 5);
|
||||
NRF24L01_ReadBuf(NRF_TX_ADDR, rx, 5);
|
||||
for (uint8_t i = 0; i < 5; i++)
|
||||
if (rx[i] != 0xA5) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- 配置 ---- */
|
||||
void NRF24L01_SetChannel(uint8_t ch) {
|
||||
if (ch > NRF_MAX_CHANNEL) ch = NRF_MAX_CHANNEL;
|
||||
nrf_channel = ch;
|
||||
NRF24L01_WriteReg(NRF_RF_CH, ch);
|
||||
}
|
||||
|
||||
void NRF24L01_SetRate(uint8_t rate) {
|
||||
uint8_t rf = NRF24L01_ReadReg(NRF_RF_SETUP);
|
||||
rf &= ~0x28;
|
||||
rf |= (rate & 0x28);
|
||||
NRF24L01_WriteReg(NRF_RF_SETUP, rf);
|
||||
}
|
||||
|
||||
void NRF24L01_SetPower(uint8_t power) {
|
||||
uint8_t rf = NRF24L01_ReadReg(NRF_RF_SETUP);
|
||||
rf &= ~0x06;
|
||||
rf |= (power & 0x06);
|
||||
NRF24L01_WriteReg(NRF_RF_SETUP, rf);
|
||||
}
|
||||
|
||||
void NRF24L01_SetTXAddr(const uint8_t *addr) {
|
||||
memcpy(nrf_tx_addr, addr, NRF_ADDR_WIDTH);
|
||||
NRF24L01_WriteBuf(NRF_TX_ADDR, addr, NRF_ADDR_WIDTH);
|
||||
}
|
||||
|
||||
void NRF24L01_SetRXAddr(uint8_t pipe, const uint8_t *addr) {
|
||||
if (pipe > 5) return;
|
||||
if (pipe < 2) {
|
||||
NRF24L01_WriteBuf(NRF_RX_ADDR_P0 + pipe, addr, NRF_ADDR_WIDTH);
|
||||
} else {
|
||||
NRF24L01_WriteReg(NRF_RX_ADDR_P0 + pipe, addr[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 模式切换 ---- */
|
||||
void NRF24L01_TXMode(void) {
|
||||
NRF_CE_L();
|
||||
uint8_t cfg = NRF24L01_ReadReg(NRF_CONFIG);
|
||||
cfg &= ~0x01; /* PRIM_RX = 0 -> TX */
|
||||
NRF24L01_WriteReg(NRF_CONFIG, cfg);
|
||||
NRF_CE_H();
|
||||
}
|
||||
|
||||
void NRF24L01_RXMode(void) {
|
||||
NRF_CE_L();
|
||||
uint8_t cfg = NRF24L01_ReadReg(NRF_CONFIG);
|
||||
cfg |= 0x01; /* PRIM_RX = 1 -> RX */
|
||||
NRF24L01_WriteReg(NRF_CONFIG, cfg);
|
||||
NRF_CE_H();
|
||||
}
|
||||
|
||||
/* ---- 发送/接收 ---- */
|
||||
uint8_t NRF24L01_TxPacket(const uint8_t *data, uint8_t len) {
|
||||
uint8_t status;
|
||||
|
||||
NRF_CE_L();
|
||||
|
||||
/* 清 TX FIFO */
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_FLUSH_TX);
|
||||
NRF_SPI_CS(1);
|
||||
|
||||
/* 写 payload */
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_W_TX_PAYLOAD);
|
||||
for (uint8_t i = 0; i < len && i < 32; i++)
|
||||
NRF_SPI_Transfer(data[i]);
|
||||
NRF_SPI_CS(1);
|
||||
|
||||
/* 发射 */
|
||||
NRF_CE_H();
|
||||
HAL_Delay(1);
|
||||
NRF_CE_L();
|
||||
|
||||
/* 等待完成 */
|
||||
uint32_t timeout = 10000;
|
||||
while (timeout--) {
|
||||
status = NRF24L01_ReadReg(NRF_STATUS);
|
||||
if (status & (NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT)) break;
|
||||
}
|
||||
|
||||
/* 清除状态 */
|
||||
NRF24L01_WriteReg(NRF_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT);
|
||||
|
||||
if (status & NRF_STATUS_TX_DS) {
|
||||
nrf_connected = 1;
|
||||
return 1; /* 发送成功 */
|
||||
}
|
||||
nrf_connected = 0;
|
||||
return 0; /* 发送失败 */
|
||||
}
|
||||
|
||||
uint8_t NRF24L01_RxPacket(uint8_t *data) {
|
||||
uint8_t status = NRF24L01_ReadReg(NRF_STATUS);
|
||||
if (!(status & NRF_STATUS_RX_DR)) return 0;
|
||||
|
||||
/* 读 payload */
|
||||
NRF_SPI_CS(0);
|
||||
NRF_SPI_Transfer(NRF_CMD_R_RX_PAYLOAD);
|
||||
for (uint8_t i = 0; i < 32; i++)
|
||||
data[i] = NRF_SPI_Transfer(0xFF);
|
||||
NRF_SPI_CS(1);
|
||||
|
||||
/* 清除 RX_DR */
|
||||
NRF24L01_WriteReg(NRF_STATUS, NRF_STATUS_RX_DR);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t NRF24L01_IsDataReady(void) {
|
||||
uint8_t status = NRF24L01_ReadReg(NRF_STATUS);
|
||||
return (status & NRF_STATUS_RX_DR) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---- 对频 ---- */
|
||||
void NRF24L01_BindMode(void) {
|
||||
/* 进入接收模式,监听对频请求 */
|
||||
NRF_CE_L();
|
||||
NRF24L01_WriteReg(NRF_CONFIG, 0x0F); /* PRX, PWR_UP, CRC */
|
||||
NRF24L01_WriteReg(NRF_EN_RXADDR, 0x01);
|
||||
NRF24L01_WriteReg(NRF_RX_PW_P0, 32);
|
||||
NRF24L01_WriteBuf(NRF_RX_ADDR_P0, nrf_tx_addr, NRF_ADDR_WIDTH);
|
||||
NRF24L01_WriteReg(NRF_STATUS, 0x70);
|
||||
NRF_CE_H();
|
||||
}
|
||||
|
||||
uint8_t NRF24L01_BindScan(uint8_t *found_addr) {
|
||||
/* 扫描所有通道寻找对频设备 */
|
||||
for (uint8_t ch = 0; ch <= NRF_MAX_CHANNEL; ch++) {
|
||||
NRF24L01_SetChannel(ch);
|
||||
HAL_Delay(2);
|
||||
if (NRF24L01_RxPacket(found_addr)) {
|
||||
/* 验证对频短语 */
|
||||
if (memcmp(found_addr, nrf_bind_phrase, strlen((char*)nrf_bind_phrase)) == 0) {
|
||||
/* 对频成功,记录地址 */
|
||||
memcpy(nrf_tx_addr, found_addr + 6, NRF_ADDR_WIDTH);
|
||||
nrf_connected = 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
257
Core/Src/oled.c
Normal file
257
Core/Src/oled.c
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* OLED SSD1306 / SSD1315 128x64 I2C 驱动实现
|
||||
*/
|
||||
#include "oled.h"
|
||||
#include "words.h"
|
||||
#include "image.h"
|
||||
#include <string.h>
|
||||
|
||||
/* 显存缓冲区 */
|
||||
uint8_t OLED_Buffer[OLED_WIDTH * OLED_HEIGHT / 8];
|
||||
|
||||
/* 内部命令发送 */
|
||||
static void OLED_WriteCmd(uint8_t cmd) {
|
||||
uint8_t buf[2] = {0x00, cmd}; /* Co=0, D/C#=0 */
|
||||
HAL_I2C_Master_Transmit(&hi2c2, OLED_ADDR << 1, buf, 2, 10);
|
||||
}
|
||||
|
||||
static void OLED_WriteData(uint8_t dat) {
|
||||
uint8_t buf[2] = {0x40, dat}; /* Co=0, D/C#=1 */
|
||||
HAL_I2C_Master_Transmit(&hi2c2, OLED_ADDR << 1, buf, 2, 10);
|
||||
}
|
||||
|
||||
void OLED_Init(void) {
|
||||
/* 通用初始化序列 (SSD1306 & SSD1315 共用) */
|
||||
OLED_WriteCmd(0xAE); /* display off */
|
||||
|
||||
OLED_WriteCmd(0xD5); /* set display clock divide */
|
||||
OLED_WriteCmd(0x80);
|
||||
OLED_WriteCmd(0xA8); /* set multiplex */
|
||||
OLED_WriteCmd(0x3F); /* 64 */
|
||||
OLED_WriteCmd(0xD3); /* set display offset */
|
||||
OLED_WriteCmd(0x00);
|
||||
OLED_WriteCmd(0x40); /* set start line */
|
||||
|
||||
#if OLED_CHIP == OLED_CHIP_SSD1306
|
||||
/* SSD1306: 外部电荷泵 */
|
||||
OLED_WriteCmd(0x8D); /* charge pump setting */
|
||||
OLED_WriteCmd(0x14); /* enable charge pump */
|
||||
#elif OLED_CHIP == OLED_CHIP_SSD1315
|
||||
/* SSD1315: 内置 DC-DC 升压 */
|
||||
OLED_WriteCmd(0xAD); /* DC-DC control */
|
||||
OLED_WriteCmd(0x8A); /* enable DC-DC (0x8A=on with 7.5V VPP) */
|
||||
/* 也可用 0x8B 关闭 DC-DC (外部供电模式) */
|
||||
#endif
|
||||
|
||||
OLED_WriteCmd(0x20); /* memory mode */
|
||||
OLED_WriteCmd(0x00); /* horizontal */
|
||||
OLED_WriteCmd(0xA1); /* segment remap (左右镜像) */
|
||||
OLED_WriteCmd(0xC8); /* COM scan direction (上下镜像) */
|
||||
|
||||
OLED_WriteCmd(0xDA); /* set COM pins */
|
||||
OLED_WriteCmd(0x12);
|
||||
OLED_WriteCmd(0x81); /* set contrast */
|
||||
OLED_WriteCmd(0xCF);
|
||||
OLED_WriteCmd(0xD9); /* set pre-charge */
|
||||
OLED_WriteCmd(0xF1);
|
||||
OLED_WriteCmd(0xDB); /* set VCOMH */
|
||||
OLED_WriteCmd(0x40);
|
||||
|
||||
OLED_WriteCmd(0xA4); /* display on resume */
|
||||
OLED_WriteCmd(0xA6); /* normal display (not inverted) */
|
||||
OLED_WriteCmd(0x2E); /* deactivate scroll */
|
||||
|
||||
OLED_WriteCmd(0xAF); /* display on */
|
||||
OLED_Clear();
|
||||
OLED_Display();
|
||||
}
|
||||
|
||||
void OLED_Clear(void) {
|
||||
memset(OLED_Buffer, 0x00, sizeof(OLED_Buffer));
|
||||
}
|
||||
|
||||
void OLED_Display(void) {
|
||||
for (uint8_t page = 0; page < OLED_PAGES; page++) {
|
||||
OLED_WriteCmd(0xB0 + page); /* set page address */
|
||||
OLED_WriteCmd(0x00); /* set lower column */
|
||||
OLED_WriteCmd(0x10); /* set higher column */
|
||||
for (uint8_t col = 0; col < OLED_WIDTH; col++) {
|
||||
OLED_WriteData(OLED_Buffer[page * OLED_WIDTH + col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OLED_SetCursor(uint8_t x, uint8_t y) {
|
||||
/* 仅用于记录,实际绘制由各函数自行计算位置 */
|
||||
(void)x; (void)y;
|
||||
}
|
||||
|
||||
void OLED_DrawPixel(uint8_t x, uint8_t y, uint8_t color) {
|
||||
if (x >= OLED_WIDTH || y >= OLED_HEIGHT) return;
|
||||
uint8_t page = y / 8;
|
||||
uint8_t bit = y % 8;
|
||||
if (color)
|
||||
OLED_Buffer[page * OLED_WIDTH + x] |= (1 << bit);
|
||||
else
|
||||
OLED_Buffer[page * OLED_WIDTH + x] &= ~(1 << bit);
|
||||
}
|
||||
|
||||
void OLED_DrawLine(uint8_t x1, uint8_t y1, uint8_t x2, uint8_t y2) {
|
||||
int16_t dx = (int16_t)x2 - x1;
|
||||
int16_t dy = (int16_t)y2 - y1;
|
||||
int16_t sx = dx > 0 ? 1 : -1;
|
||||
int16_t sy = dy > 0 ? 1 : -1;
|
||||
dx = dx > 0 ? dx : -dx;
|
||||
dy = dy > 0 ? dy : -dy;
|
||||
int16_t err = dx - dy;
|
||||
|
||||
while (1) {
|
||||
OLED_DrawPixel(x1, y1, 1);
|
||||
if (x1 == x2 && y1 == y2) break;
|
||||
int16_t e2 = err * 2;
|
||||
if (e2 > -dy) { err -= dy; x1 += sx; }
|
||||
if (e2 < dx) { err += dx; y1 += sy; }
|
||||
}
|
||||
}
|
||||
|
||||
void OLED_DrawRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h) {
|
||||
OLED_DrawLine(x, y, x + w - 1, y);
|
||||
OLED_DrawLine(x, y + h - 1, x + w - 1, y + h - 1);
|
||||
OLED_DrawLine(x, y, x, y + h - 1);
|
||||
OLED_DrawLine(x + w - 1, y, x + w - 1, y + h - 1);
|
||||
}
|
||||
|
||||
void OLED_FillRect(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t color) {
|
||||
for (uint8_t i = 0; i < w; i++)
|
||||
for (uint8_t j = 0; j < h; j++)
|
||||
OLED_DrawPixel(x + i, y + j, color);
|
||||
}
|
||||
|
||||
void OLED_DrawCircle(uint8_t cx, uint8_t cy, uint8_t r) {
|
||||
int16_t x = 0, y = r;
|
||||
int16_t d = 3 - 2 * r;
|
||||
while (x <= y) {
|
||||
OLED_DrawPixel(cx + x, cy + y, 1);
|
||||
OLED_DrawPixel(cx - x, cy + y, 1);
|
||||
OLED_DrawPixel(cx + x, cy - y, 1);
|
||||
OLED_DrawPixel(cx - x, cy - y, 1);
|
||||
OLED_DrawPixel(cx + y, cy + x, 1);
|
||||
OLED_DrawPixel(cx - y, cy + x, 1);
|
||||
OLED_DrawPixel(cx + y, cy - x, 1);
|
||||
OLED_DrawPixel(cx - y, cy - x, 1);
|
||||
if (d < 0) d += 4 * x + 6;
|
||||
else { d += 4 * (x - y) + 10; y--; }
|
||||
x++;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 文字绘制 ---- */
|
||||
|
||||
/* 6x8 ASCII 字体 (来自 words.h 的 font[][6]) */
|
||||
void OLED_ShowChar(uint8_t x, uint8_t y, char ch, uint8_t size) {
|
||||
if (size == 6) {
|
||||
/* 使用小字体 6x8 */
|
||||
uint8_t idx = (uint8_t)ch - 32;
|
||||
if (idx > 94) return;
|
||||
for (uint8_t i = 0; i < 6; i++) {
|
||||
uint8_t line = font[idx][i];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (line & (1 << j))
|
||||
OLED_DrawPixel(x + i, y + j, 1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 使用大字体 8x16 (font_big) */
|
||||
uint8_t idx = (uint8_t)ch - 32;
|
||||
if (idx > 94) return;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
uint8_t line_l = font_big[idx][i * 2];
|
||||
uint8_t line_h = font_big[idx][i * 2 + 1];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (line_l & (1 << j))
|
||||
OLED_DrawPixel(x + i, y + j, 1);
|
||||
if (line_h & (1 << j))
|
||||
OLED_DrawPixel(x + i, y + 8 + j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OLED_ShowString(uint8_t x, uint8_t y, const char *str, uint8_t size) {
|
||||
while (*str) {
|
||||
OLED_ShowChar(x, y, *str, size);
|
||||
x += size;
|
||||
if (x + size > OLED_WIDTH) { x = 0; y += (size == 6 ? 1 : 2); }
|
||||
str++;
|
||||
}
|
||||
}
|
||||
|
||||
void OLED_ShowNum(uint8_t x, uint8_t y, uint32_t num, uint8_t len, uint8_t size) {
|
||||
char buf[12];
|
||||
uint8_t i;
|
||||
for (i = 0; i < len; i++) {
|
||||
buf[len - 1 - i] = '0' + (num % 10);
|
||||
num /= 10;
|
||||
}
|
||||
buf[len] = '\0';
|
||||
OLED_ShowString(x, y, buf, size);
|
||||
}
|
||||
|
||||
/* ---- 汉字绘制 ---- */
|
||||
void OLED_ShowCN(uint8_t x, uint8_t y, uint8_t index) {
|
||||
/* hzk 数组索引: 0=空, 1=→, 2=空, 3=v, 4=←, 5=→, 6=工... */
|
||||
/* 每个汉字 12x12 点阵,占 24 bytes */
|
||||
for (uint8_t i = 0; i < 12; i++) {
|
||||
uint8_t line_l = hzk[index][i * 2];
|
||||
uint8_t line_h = hzk[index][i * 2 + 1];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (line_l & (0x80 >> j))
|
||||
OLED_DrawPixel(x + i, y + j, 1);
|
||||
if (line_h & (0x80 >> j))
|
||||
OLED_DrawPixel(x + i, y + 8 + j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 图片绘制 ---- */
|
||||
void OLED_DrawBitmap(uint8_t x, uint8_t y, const uint8_t *bmp, uint8_t w, uint8_t h) {
|
||||
uint8_t bytes_per_col = (h + 7) / 8;
|
||||
for (uint8_t i = 0; i < w; i++) {
|
||||
for (uint8_t j = 0; j < h; j++) {
|
||||
uint8_t byte_idx = i * bytes_per_col + j / 8;
|
||||
uint8_t bit_idx = j % 8;
|
||||
if (bmp[byte_idx] & (1 << bit_idx))
|
||||
OLED_DrawPixel(x + i, y + j, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 反色/滚动 ---- */
|
||||
void OLED_InvertDisplay(uint8_t invert) {
|
||||
OLED_WriteCmd(invert ? 0xA7 : 0xA6);
|
||||
}
|
||||
|
||||
void OLED_StartScrollRight(uint8_t start_page, uint8_t end_page) {
|
||||
OLED_WriteCmd(0x2E); /* stop scroll first */
|
||||
OLED_WriteCmd(0x26); /* horizontal scroll right */
|
||||
OLED_WriteCmd(0x00); /* dummy */
|
||||
OLED_WriteCmd(start_page);
|
||||
OLED_WriteCmd(0x00); /* speed */
|
||||
OLED_WriteCmd(end_page);
|
||||
OLED_WriteCmd(0x00); /* dummy */
|
||||
OLED_WriteCmd(0xFF); /* dummy */
|
||||
OLED_WriteCmd(0x2F); /* activate scroll */
|
||||
}
|
||||
|
||||
void OLED_StopScroll(void) {
|
||||
OLED_WriteCmd(0x2E);
|
||||
}
|
||||
|
||||
/* ---- 进度条 ---- */
|
||||
void OLED_DrawProgressBar(uint8_t x, uint8_t y, uint8_t w, uint8_t h, uint8_t percent) {
|
||||
if (percent > 100) percent = 100;
|
||||
OLED_DrawRect(x, y, w, h);
|
||||
uint8_t fill_w = (uint16_t)(w - 2) * percent / 100;
|
||||
if (fill_w > 0)
|
||||
OLED_FillRect(x + 1, y + 1, fill_w, h - 2, 1);
|
||||
}
|
||||
178
Core/Src/protocol.c
Normal file
178
Core/Src/protocol.c
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 协议层实现
|
||||
* NRF24 发射 / SBUS 接收 / USB HID Joystick
|
||||
*/
|
||||
#include "protocol.h"
|
||||
#include "usart.h"
|
||||
#include "usb.h"
|
||||
#include <string.h>
|
||||
|
||||
/* 回传数据 */
|
||||
uint16_t telem_voltage = 0;
|
||||
uint8_t telem_rssi = 0;
|
||||
uint8_t telem_packet_loss = 0;
|
||||
uint16_t telem_temp = 0;
|
||||
|
||||
/* 协议状态 */
|
||||
static uint32_t last_rf_send = 0;
|
||||
static uint32_t rf_interval_ms = 10; /* 默认 100Hz */
|
||||
|
||||
/* SBUS 接收缓冲 */
|
||||
static uint8_t sbus_buf[SBUS_FRAME_SIZE];
|
||||
static uint8_t sbus_idx = 0;
|
||||
static uint8_t sbus_got_frame = 0;
|
||||
|
||||
/* USB HID 报告缓冲 */
|
||||
static uint8_t hid_report[HID_REPORT_SIZE];
|
||||
|
||||
void Protocol_Init(void) {
|
||||
last_rf_send = 0;
|
||||
sbus_idx = 0;
|
||||
sbus_got_frame = 0;
|
||||
memset(hid_report, 0, sizeof(hid_report));
|
||||
}
|
||||
|
||||
/* ---- NRF24 协议 ---- */
|
||||
void Protocol_SendRFPacket(void) {
|
||||
uint8_t packet[RF_PACKET_SIZE];
|
||||
memset(packet, 0, RF_PACKET_SIZE);
|
||||
|
||||
/* 包头 */
|
||||
packet[0] = RF_SYNC_BYTE0;
|
||||
packet[1] = RF_SYNC_BYTE1;
|
||||
|
||||
/* 对频短语 (6字节) */
|
||||
memcpy(&packet[2], g_config.nrf_phrase, 6);
|
||||
|
||||
/* 通道数据 (16通道 * 2字节 = 32字节, 但只取前16字节核心通道) */
|
||||
/* 每个通道用 int16_t, 压缩为 2 bytes */
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
int16_t val = g_channels[i];
|
||||
/* 范围 -1000 ~ +1000, 映射到 0~2000 */
|
||||
uint16_t mapped = (uint16_t)(val + 1000);
|
||||
packet[8 + i * 2] = (mapped >> 8) & 0xFF;
|
||||
packet[8 + i * 2 + 1] = mapped & 0xFF;
|
||||
}
|
||||
|
||||
/* 校验和 (简单 XOR) */
|
||||
uint8_t checksum = 0;
|
||||
for (uint8_t i = 0; i < RF_PACKET_SIZE - 1; i++)
|
||||
checksum ^= packet[i];
|
||||
packet[RF_PACKET_SIZE - 1] = checksum;
|
||||
|
||||
/* 发送 */
|
||||
NRF24L01_TxPacket(packet, RF_PACKET_SIZE);
|
||||
}
|
||||
|
||||
void Protocol_ProcessRF(void) {
|
||||
/* 接收回传数据 */
|
||||
uint8_t rx_buf[32];
|
||||
if (NRF24L01_RxPacket(rx_buf)) {
|
||||
/* 解析回传包 */
|
||||
if (rx_buf[0] == 0xBB && rx_buf[1] == 0x44) {
|
||||
telem_voltage = (rx_buf[2] << 8) | rx_buf[3];
|
||||
telem_rssi = rx_buf[4];
|
||||
telem_packet_loss = rx_buf[5];
|
||||
telem_temp = (rx_buf[6] << 8) | rx_buf[7];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- SBUS 协议 ---- */
|
||||
void Protocol_SBUS_Init(void) {
|
||||
/* USART1 配置为 SBUS 模式 (100000 baud, 8E2) */
|
||||
huart1.Init.BaudRate = SBUS_BAUDRATE;
|
||||
huart1.Init.WordLength = UART_WORDLENGTH_9B;
|
||||
huart1.Init.Parity = UART_PARITY_EVEN;
|
||||
huart1.Init.StopBits = UART_STOPBITS_2;
|
||||
if (HAL_UART_Init(&huart1) != HAL_OK) {
|
||||
Error_Handler();
|
||||
}
|
||||
/* 启动接收中断 */
|
||||
HAL_UART_Receive_IT(&huart1, sbus_buf, SBUS_FRAME_SIZE);
|
||||
}
|
||||
|
||||
uint8_t Protocol_SBUS_Parse(const uint8_t *frame, int16_t *channels) {
|
||||
/* SBUS 帧: 0x0F 起始, 0x00 结束 */
|
||||
if (frame[0] != 0x0F || frame[24] != 0x00) return 0;
|
||||
|
||||
/* 解析 16 通道 (11-bit each) */
|
||||
channels[0] = ((frame[1] | frame[2]<<8) & 0x07FF);
|
||||
channels[1] = ((frame[2]>>3 | frame[3]<<5) & 0x07FF);
|
||||
channels[2] = ((frame[3]>>6 | frame[4]<<2 | frame[5]<<10) & 0x07FF);
|
||||
channels[3] = ((frame[5]>>1 | frame[6]<<7) & 0x07FF);
|
||||
channels[4] = ((frame[6]>>4 | frame[7]<<4) & 0x07FF);
|
||||
channels[5] = ((frame[7]>>7 | frame[8]<<1 | frame[9]<<9) & 0x07FF);
|
||||
channels[6] = ((frame[9]>>2 | frame[10]<<6) & 0x07FF);
|
||||
channels[7] = ((frame[10]>>5 | frame[11]<<3) & 0x07FF);
|
||||
channels[8] = ((frame[12] | frame[13]<<8) & 0x07FF);
|
||||
channels[9] = ((frame[13]>>3 | frame[14]<<5) & 0x07FF);
|
||||
channels[10] = ((frame[14]>>6 | frame[15]<<2 | frame[16]<<10) & 0x07FF);
|
||||
channels[11] = ((frame[16]>>1 | frame[17]<<7) & 0x07FF);
|
||||
channels[12] = ((frame[17]>>4 | frame[18]<<4) & 0x07FF);
|
||||
channels[13] = ((frame[18]>>7 | frame[19]<<1 | frame[20]<<9) & 0x07FF);
|
||||
channels[14] = ((frame[20]>>2 | frame[21]<<6) & 0x07FF);
|
||||
channels[15] = ((frame[21]>>5 | frame[22]<<3) & 0x07FF);
|
||||
|
||||
/* SBUS 范围: 172~1811, 映射到 -1000~+1000 */
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
channels[i] = (int16_t)((int32_t)(channels[i] - 992) * 1000 / 819);
|
||||
if (channels[i] > 1000) channels[i] = 1000;
|
||||
if (channels[i] < -1000) channels[i] = -1000;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ---- USB HID Joystick ---- */
|
||||
void Protocol_HID_Init(void) {
|
||||
/* USB 已在 MX_USB_PCD_Init 中初始化 */
|
||||
/* HID 描述符需在 usbd_hid.c 中配置 (通常由 CubeMX 生成) */
|
||||
}
|
||||
|
||||
void Protocol_HID_Send(void) {
|
||||
/* 构造 HID Joystick 报告 */
|
||||
/* 标准 Joystick 报告: 8 axis (16-bit) + buttons */
|
||||
memset(hid_report, 0, HID_REPORT_SIZE);
|
||||
|
||||
/* X, Y, Z, Rx, Ry, Rz, Slider, Dial */
|
||||
for (uint8_t i = 0; i < 8 && i < CHANNEL_COUNT; i++) {
|
||||
/* 映射 -1000~+1000 到 16-bit */
|
||||
int16_t val = g_channels[i];
|
||||
hid_report[i * 2] = val & 0xFF;
|
||||
hid_report[i * 2 + 1] = (val >> 8) & 0xFF;
|
||||
}
|
||||
|
||||
/* 发送 HID 报告 (需要 USB HID 库支持) */
|
||||
/* USBD_HID_SendReport(&hUsbDeviceFS, hid_report, HID_REPORT_SIZE); */
|
||||
}
|
||||
|
||||
/* ---- 主循环 ---- */
|
||||
void Protocol_Run(void) {
|
||||
uint32_t now = HAL_GetTick();
|
||||
|
||||
/* 根据刷新率发送 RF 数据 */
|
||||
if (now - last_rf_send >= rf_interval_ms) {
|
||||
last_rf_send = now;
|
||||
|
||||
if (g_config.rf_type == 0) {
|
||||
/* NRF24 模式 */
|
||||
Protocol_SendRFPacket();
|
||||
/* 短暂切换到接收模式收遥测 */
|
||||
NRF24L01_RXMode();
|
||||
HAL_Delay(1);
|
||||
Protocol_ProcessRF();
|
||||
NRF24L01_TXMode();
|
||||
} else {
|
||||
/* CRFS 模式 (通过 USART3 发送) */
|
||||
/* TODO: CRFS 协议实现 */
|
||||
}
|
||||
}
|
||||
|
||||
/* USB HID 发送 (每 10ms) */
|
||||
static uint32_t last_hid = 0;
|
||||
if (now - last_hid >= 10) {
|
||||
last_hid = now;
|
||||
Protocol_HID_Send();
|
||||
}
|
||||
}
|
||||
132
Core/Src/pwm_out.c
Normal file
132
Core/Src/pwm_out.c
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* PWM 输出与 LED 控制模块实现
|
||||
*/
|
||||
#include "pwm_out.h"
|
||||
|
||||
/* 全局配置 */
|
||||
uint8_t pwm1_mode = PWM_MODE_DISABLED;
|
||||
uint8_t pwm2_mode = PWM_MODE_DISABLED;
|
||||
uint8_t ws2812_effect = WS2812_MODE_RAINBOW_BREATH;
|
||||
uint8_t led_brightness = 50;
|
||||
uint8_t vibrate_level = 5;
|
||||
|
||||
/* LED 闪烁状态 */
|
||||
static uint8_t led_nrf_mode = 3; /* 0=慢闪, 1=快闪, 2=常亮, 3=灭 */
|
||||
static uint8_t led_crfs_mode = 3;
|
||||
static uint16_t led_tick = 0;
|
||||
static uint8_t led_blink_state = 0;
|
||||
|
||||
/* WS2812 动画状态 */
|
||||
static uint8_t ws2812_tick = 0;
|
||||
static uint8_t ws2812_hue = 0;
|
||||
|
||||
void PWM_Init(void) {
|
||||
/* 启动 TIM3 PWM (LED 指示灯) */
|
||||
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); /* PC6 - 系统状态 */
|
||||
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2); /* PC7 - NRF 状态 */
|
||||
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); /* PC8 - CRFS 状态 */
|
||||
|
||||
/* 初始全灭 */
|
||||
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 0);
|
||||
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, 0);
|
||||
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, 0);
|
||||
|
||||
/* TIM4 初始关闭 */
|
||||
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_3);
|
||||
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_4);
|
||||
}
|
||||
|
||||
/* ---- LED 指示灯 ---- */
|
||||
void LED_SetSystem(uint8_t on) {
|
||||
/* PC6: 系统状态 - 开机常亮 */
|
||||
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, on ? 65535 : 0);
|
||||
}
|
||||
|
||||
void LED_SetNRF(uint8_t mode) {
|
||||
led_nrf_mode = mode;
|
||||
}
|
||||
|
||||
void LED_SetCRFS(uint8_t mode) {
|
||||
led_crfs_mode = mode;
|
||||
}
|
||||
|
||||
void LED_Update(void) {
|
||||
led_tick++;
|
||||
|
||||
/* NRF LED (PC7) */
|
||||
uint16_t nrf_val = 0;
|
||||
switch (led_nrf_mode) {
|
||||
case 0: /* 慢闪: 500ms on / 500ms off */
|
||||
nrf_val = ((led_tick % 100) < 50) ? 65535 : 0;
|
||||
break;
|
||||
case 1: /* 快闪: 100ms on / 100ms off */
|
||||
nrf_val = ((led_tick % 20) < 10) ? 65535 : 0;
|
||||
break;
|
||||
case 2: /* 常亮 */
|
||||
nrf_val = 65535;
|
||||
break;
|
||||
case 3: /* 灭 */
|
||||
default:
|
||||
nrf_val = 0;
|
||||
break;
|
||||
}
|
||||
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, nrf_val);
|
||||
|
||||
/* CRFS LED (PC8) */
|
||||
uint16_t crfs_val = 0;
|
||||
switch (led_crfs_mode) {
|
||||
case 0: crfs_val = ((led_tick % 100) < 50) ? 65535 : 0; break;
|
||||
case 1: crfs_val = ((led_tick % 20) < 10) ? 65535 : 0; break;
|
||||
case 2: crfs_val = 65535; break;
|
||||
default: crfs_val = 0; break;
|
||||
}
|
||||
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_3, crfs_val);
|
||||
}
|
||||
|
||||
/* ---- PWM 输出控制 ---- */
|
||||
void PWM_SetMode(uint8_t ch, uint8_t mode) {
|
||||
if (ch == 0) {
|
||||
pwm1_mode = mode;
|
||||
if (mode == PWM_MODE_DISABLED) {
|
||||
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_3);
|
||||
} else {
|
||||
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_3);
|
||||
}
|
||||
} else {
|
||||
pwm2_mode = mode;
|
||||
if (mode == PWM_MODE_DISABLED) {
|
||||
HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_4);
|
||||
} else {
|
||||
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PWM_SetWS2812Mode(uint8_t mode) {
|
||||
if (mode >= WS2812_MODE_COUNT) return;
|
||||
ws2812_effect = mode;
|
||||
ws2812_tick = 0;
|
||||
ws2812_hue = 0;
|
||||
}
|
||||
|
||||
void PWM_SetLEDBrightness(uint8_t brightness) {
|
||||
if (brightness > 100) brightness = 100;
|
||||
led_brightness = brightness;
|
||||
uint16_t pwm_val = (uint32_t)65535 * brightness / 100;
|
||||
/* 应用到已启用的 LED 模式通道 */
|
||||
if (pwm1_mode == PWM_MODE_LED)
|
||||
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, pwm_val);
|
||||
if (pwm2_mode == PWM_MODE_LED)
|
||||
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, pwm_val);
|
||||
}
|
||||
|
||||
void PWM_SetVibrateLevel(uint8_t level) {
|
||||
if (level < VIBRATE_LEVEL_MIN) level = VIBRATE_LEVEL_MIN;
|
||||
if (level > VIBRATE_LEVEL_MAX) level = VIBRATE_LEVEL_MAX;
|
||||
vibrate_level = level;
|
||||
uint16_t pwm_val = (uint32_t)65535 * level / VIBRATE_LEVEL_MAX;
|
||||
if (pwm1_mode == PWM_MODE_VIBRATE)
|
||||
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_3, pwm_val);
|
||||
if (pwm2_mode == PWM_MODE_VIBRATE)
|
||||
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_4, pwm_val);
|
||||
}
|
||||
182
Core/Src/storage.c
Normal file
182
Core/Src/storage.c
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Flash 存储模块实现
|
||||
* 使用 STM32F1 HAL Flash 驱动
|
||||
*/
|
||||
#include "storage.h"
|
||||
|
||||
/* 全局配置 */
|
||||
Config_t g_config;
|
||||
|
||||
/* CRC32 计算 */
|
||||
static uint32_t Storage_CRC32(const uint8_t *data, uint32_t len) {
|
||||
uint32_t crc = 0xFFFFFFFF;
|
||||
for (uint32_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (crc & 1)
|
||||
crc = (crc >> 1) ^ 0xEDB88320;
|
||||
else
|
||||
crc >>= 1;
|
||||
}
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
void Storage_Init(void) {
|
||||
/* 解锁 Flash */
|
||||
HAL_FLASH_Unlock();
|
||||
}
|
||||
|
||||
void Storage_Load(void) {
|
||||
Config_t *flash_cfg = (Config_t *)FLASH_STORAGE_ADDR;
|
||||
|
||||
/* 检查魔数 */
|
||||
if (flash_cfg->magic[0] == 'L' && flash_cfg->magic[1] == 'O' &&
|
||||
flash_cfg->magic[2] == 'L' && flash_cfg->magic[3] == 'I') {
|
||||
|
||||
/* 校验 CRC */
|
||||
uint32_t calc_crc = Storage_CRC32((uint8_t*)flash_cfg,
|
||||
sizeof(Config_t) - sizeof(uint32_t));
|
||||
if (calc_crc == flash_cfg->crc32) {
|
||||
/* 有效配置, 加载 */
|
||||
memcpy(&g_config, flash_cfg, sizeof(Config_t));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* 无效或无配置, 恢复出厂 */
|
||||
Storage_Reset();
|
||||
}
|
||||
|
||||
void Storage_Save(void) {
|
||||
/* 计算 CRC */
|
||||
g_config.crc32 = Storage_CRC32((uint8_t*)&g_config,
|
||||
sizeof(Config_t) - sizeof(uint32_t));
|
||||
|
||||
/* 擦除页 */
|
||||
FLASH_EraseInitTypeDef erase = {0};
|
||||
uint32_t page_error;
|
||||
erase.TypeErase = FLASH_TYPEERASE_PAGES;
|
||||
erase.PageAddress = FLASH_STORAGE_ADDR;
|
||||
erase.NbPages = 1;
|
||||
|
||||
HAL_FLASH_Unlock();
|
||||
HAL_FLASHEx_Erase(&erase, &page_error);
|
||||
|
||||
/* 写入 (按 32-bit 写入) */
|
||||
uint32_t *src = (uint32_t*)&g_config;
|
||||
uint32_t *dst = (uint32_t*)FLASH_STORAGE_ADDR;
|
||||
uint32_t count = (sizeof(Config_t) + 3) / 4;
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
|
||||
(uint32_t)(dst + i), src[i]);
|
||||
}
|
||||
|
||||
HAL_FLASH_Lock();
|
||||
}
|
||||
|
||||
void Storage_Reset(void) {
|
||||
memset(&g_config, 0, sizeof(Config_t));
|
||||
|
||||
/* 魔数 */
|
||||
g_config.magic[0] = 'L';
|
||||
g_config.magic[1] = 'O';
|
||||
g_config.magic[2] = 'L';
|
||||
g_config.magic[3] = 'I';
|
||||
|
||||
/* 默认模型 */
|
||||
g_config.active_model = 0;
|
||||
strcpy(g_config.models[0].name, "Model1");
|
||||
g_config.models[0].type = MODEL_TYPE_AIRPLANE;
|
||||
|
||||
/* 默认通道映射: CH1->ADC_IN0, CH2->ADC_IN1, CH3->ADC_IN2, CH4->ADC_IN3 */
|
||||
for (uint8_t i = 0; i < 4; i++) {
|
||||
g_config.models[0].channel_map[i] = i;
|
||||
g_config.models[0].input_type[i] = INPUT_TYPE_ADC;
|
||||
g_config.models[0].mixer_type[i] = MIXER_TYPE_SIMPLE;
|
||||
g_config.models[0].mixer_scale[i] = 100; /* 1:1 */
|
||||
g_config.models[0].mixer_offset[i] = 0;
|
||||
}
|
||||
for (uint8_t i = 4; i < CHANNEL_COUNT; i++) {
|
||||
g_config.models[0].channel_map[i] = i;
|
||||
g_config.models[0].input_type[i] = INPUT_TYPE_NONE;
|
||||
g_config.models[0].mixer_type[i] = MIXER_TYPE_NONE;
|
||||
}
|
||||
|
||||
/* 默认高频头: NRF24 */
|
||||
g_config.rf_type = 0;
|
||||
g_config.nrf_channel = 40;
|
||||
g_config.nrf_rate = NRF_RATE_1M;
|
||||
g_config.nrf_power = NRF_PA_MAX;
|
||||
memcpy(g_config.nrf_addr, "\xE7\xE7\xE7\xE7\xE7", 5);
|
||||
strcpy(g_config.nrf_phrase, "LOVE");
|
||||
|
||||
/* CRFS 默认 */
|
||||
g_config.crfs_packet_rate = 500;
|
||||
g_config.crfs_telemetry_ratio = 4;
|
||||
g_config.crfs_power = 100;
|
||||
|
||||
/* PWM 默认 */
|
||||
g_config.pwm1_mode = PWM_MODE_DISABLED;
|
||||
g_config.pwm2_mode = PWM_MODE_DISABLED;
|
||||
g_config.ws2812_effect = WS2812_MODE_RAINBOW_BREATH;
|
||||
g_config.led_brightness = 50;
|
||||
g_config.vibrate_level = 5;
|
||||
|
||||
/* 首页默认: 回传 */
|
||||
g_config.home_page = 0;
|
||||
|
||||
/* 保存 */
|
||||
Storage_Save();
|
||||
}
|
||||
|
||||
/* ---- 模型操作 ---- */
|
||||
uint8_t Storage_ModelAdd(const char *name, uint8_t type) {
|
||||
for (uint8_t i = 0; i < MODEL_MAX_COUNT; i++) {
|
||||
if (g_config.models[i].name[0] == '\0') {
|
||||
strncpy(g_config.models[i].name, name, MODEL_NAME_LEN - 1);
|
||||
g_config.models[i].name[MODEL_NAME_LEN - 1] = '\0';
|
||||
g_config.models[i].type = type;
|
||||
|
||||
/* 默认通道映射 */
|
||||
for (uint8_t j = 0; j < 4; j++) {
|
||||
g_config.models[i].channel_map[j] = j;
|
||||
g_config.models[i].input_type[j] = INPUT_TYPE_ADC;
|
||||
g_config.models[i].mixer_type[j] = MIXER_TYPE_SIMPLE;
|
||||
g_config.models[i].mixer_scale[j] = 100;
|
||||
g_config.models[i].mixer_offset[j] = 0;
|
||||
}
|
||||
for (uint8_t j = 4; j < CHANNEL_COUNT; j++) {
|
||||
g_config.models[i].channel_map[j] = j;
|
||||
g_config.models[i].input_type[j] = INPUT_TYPE_NONE;
|
||||
g_config.models[i].mixer_type[j] = MIXER_TYPE_NONE;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0xFF; /* 已满 */
|
||||
}
|
||||
|
||||
void Storage_ModelDelete(uint8_t idx) {
|
||||
if (idx >= MODEL_MAX_COUNT) return;
|
||||
memset(&g_config.models[idx], 0, sizeof(Model_t));
|
||||
if (g_config.active_model == idx) {
|
||||
g_config.active_model = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Storage_ModelSelect(uint8_t idx) {
|
||||
if (idx >= MODEL_MAX_COUNT) return;
|
||||
if (g_config.models[idx].name[0] == '\0') return; /* 空模型 */
|
||||
g_config.active_model = idx;
|
||||
}
|
||||
|
||||
Model_t* Storage_GetActiveModel(void) {
|
||||
return &g_config.models[g_config.active_model];
|
||||
}
|
||||
|
||||
Model_t* Storage_GetModel(uint8_t idx) {
|
||||
if (idx >= MODEL_MAX_COUNT) return NULL;
|
||||
return &g_config.models[idx];
|
||||
}
|
||||
Reference in New Issue
Block a user