编程技术分享

分享编程知识,探讨技术创新

0%

简介:一款用于大学生方程式赛事的方向盘,同时兼容模拟器设备,具备无线数传功能。(开发中)

好的,作为一名高级嵌入式软件开发工程师,我将为您详细阐述这款大学生方程式赛事方向盘嵌入式系统的代码设计架构,并提供具体的C代码实现示例。这个项目旨在构建一个可靠、高效、可扩展的系统平台,满足赛车和模拟器双重应用场景,并具备无线数据传输能力。
关注微信公众号,提前获取相关推文

项目简介与需求分析

首先,让我们回顾一下项目简介和需求:

  • 产品: 大学生方程式赛事方向盘 (开发中)
  • 功能:
    • 赛车方向盘: 用于实际方程式赛车,提供实时车辆数据显示、控制输入 (换挡拨片、按钮、旋钮等)、无线数据传输等功能。
    • 模拟器兼容: 可作为PC模拟器外设使用,模拟方向盘输入,接收模拟器数据。
    • 无线数传: 实时将方向盘数据传输到地面站或其他设备,用于数据分析和监控。
  • 目标用户: 大学生方程式车队、赛车模拟器爱好者。
  • 开发阶段: 开发中,需要构建完整的嵌入式系统软件。

需求分析进一步细化:

  1. 实时性: 赛车和模拟器应用都对实时性有较高要求,数据采集、处理、显示和传输必须快速且稳定。
  2. 可靠性: 赛车环境恶劣,系统必须具备高可靠性,避免因软件故障影响比赛安全。
  3. 数据精度: 传感器数据采集需要保证一定的精度,以准确反映车辆状态。
  4. 低延迟: 输入响应延迟要尽可能低,提升驾驶体验和操作精度。
  5. 可扩展性: 系统架构应易于扩展,方便后续添加新功能或支持更多传感器和设备。
  6. 易维护性: 代码结构清晰,模块化程度高,方便后续维护和升级。
  7. 无线通信稳定性: 无线数据传输需保证稳定可靠,避免数据丢失或延迟。
  8. 功耗控制: 对于无线设备,功耗控制也是需要考虑的因素,尤其是在电池供电的场景下。
  9. 用户界面友好性: 显示界面应清晰易懂,操作逻辑简洁明了。
  10. 模拟器兼容性: 需要支持常见的赛车模拟器协议或提供通用的数据接口。

代码设计架构:分层架构与模块化设计

为了满足上述需求,我推荐采用分层架构结合模块化设计。这种架构能够有效地组织代码,提高可维护性、可扩展性和可靠性。

1. 分层架构:

我们将系统软件划分为以下几个层次,由下至上分别为:

  • 硬件抽象层 (HAL - Hardware Abstraction Layer):
    • 功能: 直接与硬件交互,封装底层硬件操作,向上层提供统一的硬件接口。
    • 模块:
      • GPIO 驱动: 控制通用输入输出引脚,用于按钮、LED 等控制。
      • SPI 驱动: 控制 SPI 接口,用于连接传感器 (如编码器、IMU)、显示屏等。
      • I2C 驱动: 控制 I2C 接口,用于连接传感器 (如气压传感器、温度传感器)。
      • ADC 驱动: 控制模数转换器,用于采集模拟信号 (如电压、电流)。
      • 定时器驱动: 提供定时器功能,用于定时任务、PWM 输出等。
      • UART 驱动: 控制串口通信,用于调试、数据传输 (如模拟器 USB 串口)。
      • 无线通信驱动 (例如 BLE, WiFi): 控制无线通信模块,用于数据传输。
      • 显示屏驱动: 控制 LCD 或 OLED 显示屏,用于数据显示。
      • 电源管理驱动: 控制电源管理相关功能,如低功耗模式。
  • 设备驱动层 (Device Driver Layer):
    • 功能: 基于 HAL 层提供的硬件接口,实现对具体设备的驱动和控制,向上层提供设备的功能接口。
    • 模块:
      • 方向盘角度传感器驱动: 读取方向盘旋转角度 (例如使用编码器或霍尔传感器)。
      • 换挡拨片驱动: 检测换挡拨片输入。
      • 按钮驱动: 检测方向盘上的按钮输入。
      • 旋钮驱动: 检测方向盘上的旋钮输入 (例如使用旋转编码器或电位器)。
      • 显示管理驱动: 管理显示屏的显示内容和刷新。
      • 无线通信管理驱动: 管理无线通信的连接、数据发送和接收。
      • 模拟器接口驱动: 处理与模拟器的通信协议和数据交换。
  • 服务层 (Service Layer):
    • 功能: 提供系统核心服务,处理业务逻辑,协调各模块之间的交互。
    • 模块:
      • 数据采集服务: 周期性地采集传感器数据,并将数据缓存或处理。
      • 输入处理服务: 处理来自换挡拨片、按钮、旋钮的输入,并转换成控制指令。
      • 显示服务: 根据采集到的数据和系统状态,生成显示内容,并调用显示管理驱动进行显示。
      • 遥测服务 (Telemetry Service): 将采集到的数据打包成遥测数据包,并调用无线通信管理驱动进行发送。
      • 模拟器通信服务: 处理与模拟器的数据交互,根据模拟器协议解析数据或发送指令。
      • 系统配置服务: 管理系统配置参数,例如采样频率、无线通信参数、显示参数等。
  • 应用层 (Application Layer):
    • 功能: 实现具体的应用逻辑,调用服务层提供的服务,完成用户需求。
    • 模块:
      • 主循环任务: 系统的核心任务,负责调度各个服务,处理事件,驱动系统运行。
      • 用户界面管理: 处理用户交互逻辑,例如菜单切换、参数设置等 (如果需要)。
      • 错误处理和日志记录: 处理系统错误,记录日志,方便调试和维护。

分层架构的优势:

  • 模块化: 每一层、每一模块职责明确,代码组织清晰。
  • 解耦: 层与层之间通过接口交互,降低耦合度,修改某一层的代码对其他层的影响较小。
  • 可移植性: HAL 层隔离了硬件差异,方便将上层代码移植到不同的硬件平台。
  • 可维护性: 模块化设计和清晰的层次结构使代码更易于理解、修改和维护。
  • 可扩展性: 可以方便地添加新的模块或功能,只需在相应的层增加模块并定义接口即可。

2. 模块化设计:

在每一层内部,我们进一步采用模块化设计,将功能划分为独立的模块。例如,在设备驱动层,方向盘角度传感器驱动、换挡拨片驱动、按钮驱动等都是独立的模块。

模块化设计的优势:

  • 代码复用: 模块可以被多个服务或应用复用。
  • 独立开发和测试: 每个模块可以独立开发、测试和验证,提高开发效率和代码质量。
  • 易于维护和升级: 修改或升级某个模块不会影响其他模块。
  • 团队协作: 模块化设计方便团队分工合作,每个人负责不同的模块开发。

C 代码实现示例 (部分模块)

为了更具体地说明代码架构,我将提供一些关键模块的 C 代码示例。请注意,以下代码仅为示例,可能需要根据具体的硬件平台和需求进行调整。

1. HAL 层 (GPIO 驱动 - 示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// hal_gpio.h
#ifndef HAL_GPIO_H
#define HAL_GPIO_H

typedef enum {
GPIO_PIN_0,
GPIO_PIN_1,
GPIO_PIN_2,
// ... 定义所有可用的 GPIO 引脚
GPIO_PIN_MAX
} GPIO_PinTypeDef;

typedef enum {
GPIO_MODE_INPUT,
GPIO_MODE_OUTPUT,
GPIO_MODE_AF, // Alternate Function
// ... 其他模式
} GPIO_ModeTypeDef;

typedef enum {
GPIO_PULL_NONE,
GPIO_PULLUP,
GPIO_PULLDOWN
} GPIO_PullTypeDef;

typedef enum {
GPIO_SPEED_LOW,
GPIO_SPEED_MEDIUM,
GPIO_SPEED_HIGH
} GPIO_SpeedTypeDef;

// 初始化 GPIO 引脚
void HAL_GPIO_Init(GPIO_PinTypeDef GPIO_Pin, GPIO_ModeTypeDef GPIO_Mode, GPIO_PullTypeDef GPIO_Pull, GPIO_SpeedTypeDef GPIO_Speed);

// 设置 GPIO 引脚输出电平
void HAL_GPIO_WritePin(GPIO_PinTypeDef GPIO_Pin, uint8_t PinState); // PinState: 0 或 1

// 读取 GPIO 引脚输入电平
uint8_t HAL_GPIO_ReadPin(GPIO_PinTypeDef GPIO_Pin); // 返回 0 或 1

#endif // HAL_GPIO_H

// hal_gpio.c (针对特定硬件平台 - 假设为 STM32)
#include "hal_gpio.h"
#include "stm32f4xx_hal.h" // 假设使用 STM32 HAL 库

void HAL_GPIO_Init(GPIO_PinTypeDef GPIO_Pin, GPIO_ModeTypeDef GPIO_Mode, GPIO_PullTypeDef GPIO_Pull, GPIO_SpeedTypeDef GPIO_Speed) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = (1 << GPIO_Pin); // 将引脚号转换为位掩码

switch (GPIO_Mode) {
case GPIO_MODE_INPUT:
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
break;
case GPIO_MODE_OUTPUT:
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
break;
case GPIO_MODE_AF:
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
break;
// ... 其他模式处理
default:
return; // 错误处理
}

switch (GPIO_Pull) {
case GPIO_PULL_NONE:
GPIO_InitStruct.Pull = GPIO_NOPULL;
break;
case GPIO_PULLUP:
GPIO_InitStruct.Pull = GPIO_PULLUP;
break;
case GPIO_PULLDOWN:
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
break;
default:
return; // 错误处理
}

switch (GPIO_Speed) {
case GPIO_SPEED_LOW:
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
break;
case GPIO_SPEED_MEDIUM:
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
break;
case GPIO_SPEED_HIGH:
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
break;
default:
return; // 错误处理
}

// 假设 GPIO 端口 A 的时钟已经使能
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); // 使用 STM32 HAL 库函数
}

void HAL_GPIO_WritePin(GPIO_PinTypeDef GPIO_Pin, uint8_t PinState) {
HAL_GPIO_WritePin(GPIOA, (1 << GPIO_Pin), (GPIO_PinState)PinState); // 使用 STM32 HAL 库函数
}

uint8_t HAL_GPIO_ReadPin(GPIO_PinTypeDef GPIO_Pin) {
return (uint8_t)HAL_GPIO_ReadPin(GPIOA, (1 << GPIO_Pin)); // 使用 STM32 HAL 库函数
}

2. 设备驱动层 (按钮驱动 - 示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// button_driver.h
#ifndef BUTTON_DRIVER_H
#define BUTTON_DRIVER_H

#include "hal_gpio.h"

typedef enum {
BUTTON_STATE_RELEASED,
BUTTON_STATE_PRESSED
} ButtonStateTypeDef;

typedef struct {
GPIO_PinTypeDef gpio_pin;
uint8_t active_level; // 按钮按下时的电平 (0 或 1)
uint32_t debounce_time_ms; // 消抖时间 (毫秒)
ButtonStateTypeDef current_state;
ButtonStateTypeDef last_state;
uint32_t last_debounce_time;
} Button_t;

// 初始化按钮驱动
void Button_Init(Button_t *button, GPIO_PinTypeDef gpio_pin, uint8_t active_level, uint32_t debounce_time_ms);

// 获取按钮状态
ButtonStateTypeDef Button_GetState(Button_t *button);

#endif // BUTTON_DRIVER_H

// button_driver.c
#include "button_driver.h"
#include "cmsis_os.h" // 假设使用 FreeRTOS 或其他 RTOS

void Button_Init(Button_t *button, GPIO_PinTypeDef gpio_pin, uint8_t active_level, uint32_t debounce_time_ms) {
button->gpio_pin = gpio_pin;
button->active_level = active_level;
button->debounce_time_ms = debounce_time_ms;
button->current_state = BUTTON_STATE_RELEASED;
button->last_state = BUTTON_STATE_RELEASED;
button->last_debounce_time = osKernelGetTickCount(); // 获取当前系统时间
HAL_GPIO_Init(gpio_pin, GPIO_MODE_INPUT, GPIO_PULLUP, GPIO_SPEED_LOW); // 初始化 GPIO 为输入,上拉
}

ButtonStateTypeDef Button_GetState(Button_t *button) {
uint8_t raw_state = HAL_GPIO_ReadPin(button->gpio_pin);
ButtonStateTypeDef current_button_state;

if (raw_state == button->active_level) { // 按钮按下
current_button_state = BUTTON_STATE_PRESSED;
} else { // 按钮释放
current_button_state = BUTTON_STATE_RELEASED;
}

if (current_button_state != button->last_state) { // 状态发生变化
if ((osKernelGetTickCount() - button->last_debounce_time) >= button->debounce_time_ms) { // 消抖时间已过
button->current_state = current_button_state;
button->last_state = current_button_state;
button->last_debounce_time = osKernelGetTickCount();
}
}
return button->current_state;
}

3. 服务层 (输入处理服务 - 示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// input_service.h
#ifndef INPUT_SERVICE_H
#define INPUT_SERVICE_H

#include "button_driver.h"
// ... 其他输入设备驱动头文件

typedef struct {
Button_t button_shift_up;
Button_t button_shift_down;
// ... 其他按钮、旋钮等输入
} InputData_t;

// 初始化输入服务
void InputService_Init(void);

// 获取输入数据
InputData_t InputService_GetData(void);

#endif // INPUT_SERVICE_H

// input_service.c
#include "input_service.h"
#include "button_driver.h"
// ... 其他输入设备驱动头文件

InputData_t current_input_data;

void InputService_Init(void) {
// 初始化换挡拨片按钮
Button_Init(&current_input_data.button_shift_up, GPIO_PIN_0, 0, 50); // 假设 GPIO_PIN_0 连接升档拨片,低电平有效,消抖 50ms
Button_Init(&current_input_data.button_shift_down, GPIO_PIN_1, 0, 50); // 假设 GPIO_PIN_1 连接降档拨片,低电平有效,消抖 50ms
// ... 初始化其他按钮、旋钮等
}

InputData_t InputService_GetData(void) {
// 更新按钮状态
Button_GetState(&current_input_data.button_shift_up);
Button_GetState(&current_input_data.button_shift_down);
// ... 更新其他输入设备状态

return current_input_data;
}

4. 应用层 (主循环任务 - 示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// main.c
#include "main.h"
#include "input_service.h"
#include "display_service.h"
#include "telemetry_service.h"
#include "simulator_service.h"
#include "cmsis_os.h" // 假设使用 FreeRTOS 或其他 RTOS

void SystemClock_Config(void); // 时钟配置 (根据具体硬件平台实现)
void MX_GPIO_Init(void); // GPIO 初始化 (根据具体硬件平台实现)
// ... 其他硬件初始化函数

void DefaultTask_Start(void *argument); // 主循环任务函数

int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
// ... 其他硬件初始化

// 服务初始化
InputService_Init();
DisplayService_Init();
TelemetryService_Init();
SimulatorService_Init();

// 创建主循环任务
osThreadDef(defaultTask, DefaultTask_Start, osPriorityNormal, 0, 128); // 定义任务
osThreadCreate(osThread(defaultTask), NULL); // 创建任务

// 启动 RTOS 调度器
osKernelStart();

while (1) {
// 不应该执行到这里
}
}

void DefaultTask_Start(void *argument) {
InputData_t input_data;
TelemetryData_t telemetry_data;
SimulatorData_t simulator_data;

for (;;) {
// 1. 数据采集 (从传感器驱动获取数据,例如方向盘角度、速度等)
// ...

// 2. 输入处理 (获取按钮、旋钮等输入)
input_data = InputService_GetData();
if (Button_GetState(&input_data.button_shift_up) == BUTTON_STATE_PRESSED) {
// 执行升档操作
// ...
}
if (Button_GetState(&input_data.button_shift_down) == BUTTON_STATE_PRESSED) {
// 执行降档操作
// ...
}
// ... 处理其他输入

// 3. 数据处理 (根据采集到的数据和输入进行计算和逻辑处理)
// ...

// 4. 显示更新 (将需要显示的数据传递给显示服务)
DisplayService_SetData(telemetry_data, simulator_data); // 假设显示服务可以同时显示遥测和模拟器数据
DisplayService_UpdateDisplay();

// 5. 遥测数据发送 (将遥测数据传递给遥测服务)
TelemetryService_SendData(telemetry_data);

// 6. 模拟器数据处理 (如果连接到模拟器,则进行模拟器数据交换)
SimulatorService_ProcessData(&simulator_data);

osDelay(10); // 任务延时,控制循环频率 (例如 10ms 周期)
}
}

项目中采用的技术和方法 (实践验证)

  • 微控制器: 推荐使用 ARM Cortex-M 系列 微控制器,例如 STM32 系列。STM32 具有丰富的 Peripherals,强大的处理能力,以及完善的开发生态,非常适合嵌入式系统开发。
  • 实时操作系统 (RTOS): 使用 FreeRTOSRT-Thread 等轻量级 RTOS 可以有效地管理任务调度、资源分配、时间管理,提高系统的实时性和可靠性。
  • C 语言: C 语言是嵌入式系统开发的主流语言,具有高效、灵活、可移植性好等优点。
  • HAL 库: 使用厂商提供的 HAL 库 (例如 STM32 HAL 库) 可以简化硬件驱动开发,提高开发效率,并增强代码的可移植性。
  • 模块化编程: 采用模块化编程思想,将系统划分为独立的模块,提高代码的可维护性和可复用性。
  • 事件驱动编程: 在服务层和应用层可以使用事件驱动编程模型,提高系统的响应性和效率。例如,可以使用消息队列或信号量来实现事件通知和处理。
  • 状态机: 对于复杂的逻辑控制,可以使用状态机模型进行设计,例如方向盘的工作模式切换、显示界面的状态切换等。
  • 数据结构和算法: 合理选择数据结构 (例如队列、链表、环形缓冲区) 和算法 (例如滤波算法、PID 控制算法) 可以提高系统的性能和效率。
  • 无线通信技术: 根据需求选择合适的无线通信技术,例如 蓝牙 BLE (低功耗蓝牙) 或 WiFi。BLE 适合低功耗、短距离数据传输,WiFi 适合高带宽、远距离数据传输。
  • USB 通信: 使用 USB 串口 (CDC-ACM)USB HID 协议实现与 PC 模拟器的通信。USB 串口实现简单,通用性好;USB HID 可以模拟标准 HID 设备 (例如游戏手柄),兼容性更好。
  • 显示技术: 选择合适的显示屏,例如 OLEDTFT LCD。OLED 具有高对比度、低功耗等优点,TFT LCD 具有成本低、色彩丰富等优点。
  • 版本控制: 使用 Git 等版本控制工具管理代码,方便团队协作、代码管理和版本回溯。
  • 调试方法: 使用 JTAG/SWD 调试器 进行硬件调试,使用 串口打印日志记录 进行软件调试。
  • 测试方法: 进行 单元测试集成测试系统测试实际场景测试,保证系统的功能和性能满足需求。

系统扩展性考虑

为了保证系统的可扩展性,在架构设计和代码实现时需要考虑以下几点:

  • 接口设计: 模块之间的接口要定义清晰、稳定,方便后续添加新的模块或功能。
  • 配置参数化: 将系统配置参数 (例如采样频率、通信参数、显示参数) 放在配置文件或可配置的结构体中,方便修改和调整。
  • 预留扩展接口: 在硬件和软件设计上预留扩展接口,例如预留更多的 GPIO 引脚、通信接口、传感器接口等。
  • 模块化设计: 模块化设计本身就具有良好的扩展性,可以方便地添加新的模块或替换现有的模块。
  • 软件升级机制: 考虑实现 OTA (Over-The-Air) 固件升级功能,方便远程升级和维护。

总结

这款大学生方程式赛事方向盘嵌入式系统的软件开发是一个综合性的项目,需要考虑实时性、可靠性、可扩展性、易维护性等多个方面。采用分层架构和模块化设计是构建可靠、高效、可扩展系统的关键。

我提供的 C 代码示例只是冰山一角,实际项目中需要根据具体的硬件平台、传感器选型、无线通信方案、显示屏类型、模拟器协议等进行详细的设计和实现。同时,需要经过充分的测试和验证,确保系统能够稳定可靠地运行。

希望这份详细的架构设计和代码示例能够为您提供有价值的参考。在实际开发过程中,请根据具体情况进行调整和优化,并不断学习和实践,提升嵌入式系统开发能力。 如果您有任何进一步的问题,欢迎随时提出。

欢迎关注我的其它发布渠道