例子 void (*set_speed_left)(int16_t speed); 它完美体现了硬件抽象层的思想。让我为你详细拆解,并用控制小车上两个不同电机的例子,一步一步解释。
一 这个例子是干什么的?
void (*set_speed_left)(int16_t speed);
这行代码的意思是:声明了一个名为 set_speed_left 的“遥控器”。这个遥控器专门用来“遥控”一个功能,这个功能要接收一个 int16_t 类型的速度值,并且不返回任何东西 (void)。
它的核心目的是:将 “设置左轮速度”这个操作,与具体如何实现这个操作的代码分离开。
二 典型应用场景:控制两个不同的电机
假设你的STM32小车有两个轮子,但左边电机由定时器1的PWM通道1驱动,右边电机由定时器2的PWM通道3驱动。如果不使用函数指针,你的代码可能是这样的:
// 传统方式,代码与硬件高度绑定
void SetMotorLeft_Speed(int16_t speed) {
// 一大堆设置TIM1->CCR1寄存器的代码...
}
void SetMotorRight_Speed(int16_t speed) {
// 一大堆设置TIM2->CCR3寄存器的代码...
}
void Car_SetSpeed(int16_t left, int16_t right) {
SetMotorLeft_Speed(left); // 这里固定死了必须用左电机函数
SetMotorRight_Speed(right); // 这里固定死了必须用右电机函数
}
这种方式的问题在于,Car_SetSpeed 这个上层控制函数必须知道下面有两个具体函数,并且知道谁管左、谁管右。如果电机换了或者驱动方式变了,上层代码也得跟着改。
使用函数指针后,代码会变得非常灵活和清晰:
三 完整代码示例:用函数指针抽象电机控制
/* 步骤1: 定义统一的“遥控器”类型 */
// 定义一种函数指针类型,它代表“设置速度”这个操作
typedef void (*MotorSpeedSetter_t)(int16_t speed);
/* 步骤2: 实现具体的底层驱动函数(准备可以被遥控的“设备”) */
// 具体控制左电机的函数
void PWM_Set_TIM1_CCR1(int16_t speed) {
if(speed >= 0) {
TIM1->CCR1 = speed; // 正转,设置PWM占空比
} else {
TIM1->CCR1 = -speed; // 反转
}
// 可能还有设置方向引脚电平的代码...
}
// 具体控制右电机的函数
void PWM_Set_TIM2_CCR3(int16_t speed) {
// 这是另一个定时器的另一个通道,硬件操作完全不同
if(speed >= 0) {
TIM2->CCR3 = speed;
} else {
TIM2->CCR3 = -speed;
}
// 设置方向引脚...
}
/* 步骤3: 创建“遥控器”并绑定“设备” */
// 声明两个“速度设置遥控器”
MotorSpeedSetter_t set_left_motor_speed = NULL;
MotorSpeedSetter_t set_right_motor_speed = NULL;
// 初始化函数,在这里把“遥控器”绑定到具体的“设备”(函数)
void Motor_Driver_Init(void) {
set_left_motor_speed = PWM_Set_TIM1_CCR1; // 遥控器1绑定左电机驱动
set_right_motor_speed = PWM_Set_TIM2_CCR3; // 遥控器2绑定右电机驱动
}
/* 步骤4: 上层应用代码(使用“遥控器”来统一控制) */
// 小车控制模块。它完全不知道下面具体是怎么控制电机的!
void Car_SetSpeed(int16_t left_speed, int16_t right_speed) {
// 关键:这里只是按下“遥控器”按钮,而不管遥控的是谁
if(set_left_motor_speed != NULL) {
set_left_motor_speed(left_speed); // 遥控左电机
}
if(set_right_motor_speed != NULL) {
set_right_motor_speed(right_speed); // 遥控右电机
}
}
// 更高级的控制逻辑:让小车原地旋转
void Car_Spin(int16_t speed) {
Car_SetSpeed(speed, -speed); // 左轮正转,右轮反转
}
int main(void) {
// 初始化硬件和驱动
HAL_Init();
MX_TIM1_Init(); // 初始化定时器1
MX_TIM2_Init(); // 初始化定时器2
Motor_Driver_Init(); // 关键:绑定函数指针!
while(1) {
// 示例:让小车前进、旋转、后退
Car_SetSpeed(300, 300); // 前进
HAL_Delay(1000);
Car_Spin(200); // 原地旋转
HAL_Delay(1000);
Car_SetSpeed(-200, -200); // 后退
HAL_Delay(1000);
Car_SetSpeed(0, 0); // 停止
HAL_Delay(1000);
}
}
四 这样做带来的巨大好处
-
上层代码(
Car_SetSpeed)变得极其干净:它只知道要“设置速度”,完全不知道速度是通过PWM、DAC还是直接给IO口发脉冲实现的。这实现了硬件隔离。 -
更换硬件极其方便:如果明天左电机坏了,你换了一个需要不同驱动方式的新电机,你只需要做一件事:
// 实现新的驱动函数 void NewMotor_Driver_Left(int16_t speed) { // 新电机的驱动代码... } // 然后在初始化时重新绑定 void Motor_Driver_Init(void) { set_left_motor_speed = NewMotor_Driver_Left; // 只改这一行! set_right_motor_speed = PWM_Set_TIM2_CCR3; }所有上层代码(
Car_SetSpeed,Car_Spin等)一行都不用改! -
便于测试和模拟:在电脑上仿真时,你可以绑定一个“模拟电机”函数,这个函数只是把速度值打印到屏幕上,而不需要真实的硬件。
五 使用中的关键点(针对这个例子)
-
为什么用
int16_t?
因为电机速度通常有正负(表示方向)。int16_t(16位有符号整数)的范围是 -32768 到 32767,足够表示速度值,且能明确表达方向。 -
调用前的安全检查:
if(set_left_motor_speed != NULL) { set_left_motor_speed(100); }这个检查至关重要。如果指针是
NULL(比如你忘了调用Motor_Driver_Init),程序会立即崩溃。 -
实际工程中的优化:
实际项目中,我们通常会把电机相关的所有操作封装成一个结构体:typedef struct { MotorSpeedSetter_t set_speed; void (*brake)(void); // 刹车函数指针 void (*coast)(void); // 滑行函数指针 int16_t current_speed; // 当前速度 } MotorController_t; MotorController_t left_motor, right_motor; // 这样管理起来更系统化
六 总结:例子的精髓
void (*set_speed_left)(int16_t speed); 这个声明,实际上是在建立一个契约或接口:
-
契约内容:“我承诺会提供一个方法,你给它一个速度值,它就能让左轮转起来。”
-
具体如何实现:“可能是PWM,可能是模拟电压,也可能只是打印一条日志——这是我的自由,你(上层调用者)不用管。”
这就是面向接口编程的思想在C语言中的体现,也是写出高质量、可维护的嵌入式代码的关键技巧之一。
803

被折叠的 条评论
为什么被折叠?



