2. 硬件抽象层设计:HAL层接口定义、电机驱动芯片选型、GPIO与定时器配置

好,咱们进入第二章。这一章,说白了就是给按摩仪的“灵魂”搭骨架。你想想看,如果代码直接操作寄存器,换个芯片就得重写一遍,那得多痛苦?所以,硬件抽象层(HAL)就是干这个的——把硬件细节藏起来,让上层代码只关心“我要振动多强”,而不是“我要往哪个寄存器写0x55”。

我个人习惯,做嵌入式项目第一件事就是画HAL接口。不是画UML那种高大上的图,而是用脑子想清楚:这个模块到底要暴露哪些功能给上层?想清楚了,后面写代码才不拧巴。

2.1 HAL层接口定义:让上层代码“无感”硬件

HAL层,说白了就是一层“翻译官”。上层说“我要振动”,HAL层就去操作电机驱动芯片。上层说“我要停”,HAL层就去关定时器。上层代码永远不需要知道电机是PWM控制的还是GPIO高低电平控制的。

我建议,接口定义要遵循“最小完备”原则。什么叫最小完备?就是接口数量刚刚好,不多一个也不少一个。多了,上层调用者容易懵;少了,功能又不够用。

对于按摩仪振动反馈,核心接口其实就这几个:

  • 初始化接口:配置GPIO、定时器、电机驱动芯片的初始状态。
  • 设置振动强度:传入0-100的整数,对应PWM占空比。
  • 设置振动模式:连续、脉冲、渐变等。
  • 启动/停止:控制电机是否工作。
  • 获取状态:当前是否运行、当前强度等。

嗯,这里要注意,接口参数一定要用“物理意义”而不是“寄存器值”。比如强度用百分比,而不是PWM的计数值。这样上层代码才可读、可移植。

核心原则:HAL接口的输入输出,必须是人能看懂的单位。百分比、毫秒、赫兹,而不是0x1234这种魔法数字。

我曾经在一个项目中,接手了别人的代码,发现HAL接口传的是PWM的ARR值和CCR值。上层代码里全是“set_pwm(1000, 500)”这种调用,鬼知道1000和500代表什么强度?后来我花了整整两天才把所有调用点改完。从那以后,我定了个规矩:HAL接口里绝对不允许出现寄存器级别的参数。

2.2 电机驱动芯片选型:别小看这颗小芯片

电机驱动芯片,说白了就是“功率放大器”。MCU的GPIO只能输出几毫安电流,带不动电机。驱动芯片就是干这个的——把MCU的小信号放大成能驱动电机的大电流。

选型时,我一般看这几个关键参数:

参数 说明 我的建议值
工作电压 电机额定电压,通常3.3V或5V 3.3V-5V,兼容锂电池供电
输出电流 电机堵转时的最大电流 至少2倍电机额定电流
PWM频率 驱动芯片支持的PWM输入频率 20kHz以上,避免人耳听到啸叫
控制接口 GPIO直连还是I2C/SPI GPIO直连,简单可靠
保护功能 过流、过热、欠压锁定 必须有,不然烧芯片是常事

我个人比较喜欢用DRV8837或者类似的H桥驱动芯片。为什么?因为它内置了电流调节功能,而且PWM频率可以到250kHz,完全听不到噪音。你想想看,如果按摩仪发出“滋滋”的啸叫声,用户体验得多差?

小技巧:选型时,优先选封装大一点的。比如SOP-8比DFN-8好焊多了。我在项目里吃过DFN封装的亏,手工焊接时连焊了好几次,最后不得不重新打板。嗯,教训深刻。

2.3 GPIO与定时器配置:让硬件“听话”

GPIO配置,听起来简单,但坑不少。我见过太多人把GPIO配置成推挽输出,结果驱动芯片的使能引脚需要开漏,导致逻辑电平不对,电机死活不转。

对于按摩仪,GPIO配置一般分三类:

  • PWM输出引脚:配置为复用功能推挽输出,连接到定时器的通道。
  • 使能引脚:配置为普通推挽输出,控制驱动芯片的EN引脚。
  • 方向引脚:配置为普通推挽输出,控制电机正反转(如果支持)。

定时器配置,这才是重头戏。PWM的频率和分辨率直接决定了振动的细腻程度。

我建议,PWM频率选20kHz以上。为什么?因为人耳能听到的频率范围是20Hz-20kHz。低于20kHz,电机转动时会产生可听见的啸叫声。20kHz以上,人耳就听不到了,只有触觉上的振动感。

定时器的配置代码,我一般这样写:

// 以STM32为例,配置TIM2的通道1输出PWM
void HAL_Motor_Init(void) {
    // 1. 使能定时器时钟
    __HAL_RCC_TIM2_CLK_ENABLE();
    
    // 2. 配置GPIO为复用功能
    GPIO_InitTypeDef gpio = {0};
    gpio.Pin = GPIO_PIN_0;          // PA0, TIM2_CH1
    gpio.Mode = GPIO_MODE_AF_PP;    // 复用推挽输出
    gpio.Pull = GPIO_NOPULL;
    gpio.Speed = GPIO_SPEED_FREQ_HIGH;
    gpio.Alternate = GPIO_AF1_TIM2;
    HAL_GPIO_Init(GPIOA, &gpio);
    
    // 3. 配置定时器基础参数
    TIM_HandleTypeDef htim = {0};
    htim.Instance = TIM2;
    htim.Init.Prescaler = 84 - 1;       // 84MHz / 84 = 1MHz
    htim.Init.Period = 50 - 1;          // 1MHz / 50 = 20kHz
    htim.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim);
    
    // 4. 配置PWM通道
    TIM_OC_InitTypeDef oc = {0};
    oc.OCMode = TIM_OCMODE_PWM1;
    oc.Pulse = 25;                      // 50%占空比
    oc.OCPolarity = TIM_OCPOLARITY_HIGH;
    oc.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim, &oc, TIM_CHANNEL_1);
    
    // 5. 启动PWM输出
    HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_1);
}

嗯,这里要注意预分频器和自动重装载值的计算。我一般先算定时器时钟频率,再算PWM频率。比如系统时钟84MHz,预分频84,得到1MHz的计数频率。自动重装载值设为50,PWM频率就是1MHz/50=20kHz。分辨率呢?就是50级,从0到49,对应0%到100%的占空比。

避坑指南:我曾经在配置定时器时,忘了使能定时器的时钟。结果代码跑起来,PWM死活没输出。查了整整一个下午,最后发现是RCC时钟使能没写。从那以后,我写定时器配置的第一行永远是使能时钟,成了肌肉记忆。

还有一个容易忽略的点:GPIO的复用功能映射。不同芯片、不同引脚,复用功能编号不一样。比如PA0在STM32F103上是TIM2_CH1,但在STM32F407上可能就是TIM5_CH1。所以,配置前一定要查数据手册,确认复用功能编号。

我个人习惯,把GPIO和定时器的配置封装成一个函数,比如HAL_Motor_Init()。这样上层调用时,只需要在初始化阶段调用一次,后面就只管设置强度和模式了。代码干净,逻辑清晰。

好了,这一章的内容就这些。HAL层接口定义、驱动芯片选型、GPIO和定时器配置,这三块搞定了,按摩仪的硬件基础就算打牢了。下一章,咱们聊聊PWM控制算法,怎么让振动从“傻震”变成“智能震”。