STM32C8T6实战:用PWM信号精准驾驭激光雷达的启动与休眠

在嵌入式开发的世界里,激光雷达正从实验室和高端机器人领域,逐步走向更广泛的消费级应用,从扫地机器人到智能小车,再到各种创意互动装置,都能见到它的身影。对于许多开发者而言,如何让这颗“眼睛”听话地工作,尤其是如何精确地控制它的启动和停止,是项目成功的关键一步。这不仅仅是发送一个简单的开关指令,更涉及到对电机驱动逻辑的深刻理解和对PWM(脉冲宽度调制)信号的精细操控。STM32C8T6,这颗经典的ARM Cortex-M3内核微控制器,以其丰富的外设和极高的性价比,成为了实现这一目标的理想平台。本文将带你深入探讨如何利用STM32C8T6的定时器生成PWM信号,实现对激光雷达电机启停的精准、可靠控制,并融合串口通信进行数据交互,为你呈现一套从原理到实践的完整解决方案。

1. 理解激光雷达的驱动核心:PWM与电机控制

激光雷达,尤其是基于三角测距或飞行时间(ToF)原理的消费级型号,其核心扫描部件通常由一个微型直流电机驱动。这个电机带动激光发射器和接收器旋转,从而实现对周围环境的360度扫描。因此,控制激光雷达,本质上就是控制这个电机。

PWM 在这里扮演了“调速开关”的角色。它通过快速切换高低电平,改变在一个周期内高电平所占的时间比例(即占空比),来模拟不同的平均电压。对于直流电机而言,占空比直接决定了其转速和扭矩。当我们用PWM信号控制激光雷达的电机时:

  • 高占空比(如90%-100%):相当于施加一个接近满额的电压,电机全力启动,带动雷达开始高速、稳定扫描。
  • 低占空比或零占空比(0%):相当于施加极低或零电压,电机停止转动,雷达进入休眠或待机状态。

这种控制方式的优势在于高效和精准。相比简单的GPIO高低电平控制,PWM允许我们实现软启动、软停止,减少电流冲击,保护电机和驱动电路,同时也能在需要时进行调速。STM32C8T6的通用定时器(如TIM2, TIM3, TIM4)天生就是为生成PWM而设计的,配置灵活,精度高。

提示:在动手编码前,务必查阅你所使用的激光雷达的具体数据手册。不同品牌、型号的雷达,其电机驱动电压、PWM控制逻辑(是高电平有效还是低电平有效)、以及所需的控制信号频率可能完全不同。错误的参数可能导致雷达不工作甚至损坏。

2. STM32C8T6的PWM配置实战

我们以STM32C8T6的TIM2定时器通道1(对应PA0引脚)为例,详细讲解如何配置一个用于控制激光雷达电机的PWM输出。这里假设我们需要一个频率为1kHz,初始占空比为0%(即停止状态)的PWM信号。

2.1 硬件连接与引脚规划

首先,我们需要进行硬件连接。激光雷达模块通常会有电源(VCC, GND)、串口通信(TX, RX)以及电机控制(PWM)引脚。对于STM32C8T6:

  1. PWM控制线:连接激光雷达的电机使能或PWM输入引脚到STM32的 PA0 (TIM2_CH1)。
  2. 串口通信线:雷达的数据输出(TX)接STM32的 PA3 (USART2_RX),STM32的指令发送(TX)接雷达的RX引脚,即 PA2 (USART2_TX)。这里使用USART2是为了与程序下载调试用的USART1(PA9/PA10)分开。
  3. 电源:确保为激光雷达提供稳定、足额的5V或3.3V电源(根据雷达型号),电流能力要足够驱动电机,通常建议单独供电或使用性能良好的稳压模块。

2.2 定时器PWM初始化代码解析

下面是一段基于标准外设库(Standard Peripheral Library)的初始化代码。我们将逐步拆解其关键步骤。

#include "stm32f10x.h"

void TIM2_PWM_Init(u16 arr, u16 psc) {
    GPIO_InitTypeDef GPIO_InitStructure;
    TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
    TIM_OCInitTypeDef TIM_OCInitStructure;

    // 1. 开启时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 初始化GPIOA.0为复用推挽输出
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. 初始化定时器时基单元
    // 定时器时钟频率 = 72MHz / (psc+1)
    // 自动重装载值arr决定PWM频率: PWM频率 = 定时器时钟频率 / (arr+1)
    TIM_TimeBaseStructure.TIM_Period = arr; // 自动重装载值
    TIM_TimeBaseStructure.TIM_Prescaler = psc; // 预分频系数
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
    TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    // 4. 初始化PWM输出模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; // PWM模式1
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; // 使能输出
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; // 输出极性高
    TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比,设置为0,电机停止
    TIM_OC1Init(TIM2, &TIM_OCInitStructure); // 初始化通道1

    // 5. 使能预装载寄存器
    TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM2, ENABLE);

    // 6. 启动定时器
    TIM_Cmd(TIM2, ENABLE);
}

关键参数计算与解释:

  • TIM_Prescaler (psc): 预分频器,用于降低定时器的时钟频率。STM32C8T6的APB1总线定时器时钟通常为72MHz。设置psc=71,则定时器实际时钟 = 72MHz / (71+1) = 1MHz。
  • TIM_Period (arr): 自动重装载值,决定PWM的周期。结合上面的时钟,PWM频率 = 1MHz / (arr+1)。设置arr=999,则PWM频率 = 1MHz / 1000 = 1kHz。这是一个常见的电机控制频率。
  • TIM_Pulse: 捕获/比较寄存器的值,直接决定PWM的占空比。占空比 = (TIM_Pulse / (arr+1)) * 100%。在初始化时我们设为0,让雷达保持停止状态。

在主函数中调用 TIM2_PWM_Init(999, 71); 即可完成初始化。

2.3 动态控制PWM占空比

初始化后,我们可以通过一个简单的函数来动态调整占空比,从而控制电机的启停和速度。

// 设置TIM2通道1的PWM占空比
// compare: 比较值,范围0~arr,0为停止,arr为全速
void LaserRadar_PWM_Set(u16 compare) {
    TIM_SetCompare1(TIM2, compare);
}

在实际应用中:

  • 启动雷达LaserRadar_PWM_Set(900); // 设置高占空比(例如90%),电机全速启动。
  • 停止雷达LaserRadar_PWM_Set(0); // 设置占空比为0,电机停止。

有些雷达可能需要一个特定的PWM占空比范围来代表“启动”,例如50%-100%代表不同转速,0%-10%代表停止。这需要根据具体雷达手册调整。

3. 融合串口通信:指令与数据的协同

仅有PWM控制电机转动还不够,我们还需要通过串口与雷达“对话”,发送开始扫描、停止扫描等指令,并接收它返回的距离和角度数据。这是一个典型的控制流与数据流分离的架构:PWM控制物理电机,串口控制逻辑功能。

3.1 串口初始化与指令发送

我们使用USART2与雷达通信。初始化过程与USART1类似,但引脚不同。

void USART2_Init(u32 bound) {
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);

    // USART2_TX -> PA2
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    // USART2_RX -> PA3
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    USART_InitStructure.USART_BaudRate = bound; // 波特率,需与雷达匹配,常见115200
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;
    USART_InitStructure.USART_StopBits = USART_StopBits_1;
    USART_InitStructure.USART_Parity = USART_Parity_No;
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
    USART_Init(USART2, &USART_InitStructure);

    USART_Cmd(USART2, ENABLE);
}

对于像思岚(Slamtec)RPLIDAR A1这类常见雷达,其控制指令是固定的数据包。例如:

// 雷达控制指令定义
#define SCAN_CMD_LEN 2
#define STOP_CMD_LEN 2

const uint8_t scan_cmd[SCAN_CMD_LEN] = {0xA5, 0x20}; // 开始扫描指令
const uint8_t stop_cmd[STOP_CMD_LEN] = {0xA5, 0x25}; // 停止扫描指令

// 发送指令函数
void LaserRadar_Send_Cmd(const uint8_t *cmd, uint8_t len) {
    for(uint8_t i=0; i<len; i++) {
        USART_SendData(USART2, cmd[i]);
        while(USART_GetFlagStatus(USART2, USART_FLAG_TC) == RESET); // 等待发送完成
    }
}

3.2 数据接收与解析策略

激光雷达的数据输出是连续且高速的。不建议在串口接收中断中处理复杂的解析逻辑,因为这样很容易导致中断阻塞,影响系统实时性。更推荐采用查询标志位的方式在主循环中处理。

雷达数据包通常有固定的帧结构。以常见的单点数据包为例,它可能包含起始标志、质量、角度、距离等信息。解析的关键在于状态机的运用。

typedef enum {
    DATA_WAIT_SYNC = 0,
    DATA_READ_QUALITY,
    DATA_READ_ANGLE_L,
    DATA_READ_ANGLE_H,
    DATA_READ_DIST_L,
    DATA_READ_DIST_H,
    DATA_PACKAGE_COMPLETE
} ParserState_t;

ParserState_t parse_state = DATA_WAIT_SYNC;
uint8_t raw_data_buffer[6];
uint16_t angle, distance;

void LaserRadar_Data_Parser(uint8_t byte) {
    static uint8_t data_index = 0;

    switch(parse_state) {
        case DATA_WAIT_SYNC:
            if(byte == 0xFA) { // 假设0xFA是帧头
                raw_data_buffer[data_index++] = byte;
                parse_state = DATA_READ_QUALITY;
            }
            break;
        case DATA_READ_QUALITY:
            raw_data_buffer[data_index++] = byte;
            if((byte & 0x01) == 0) { // 简单的奇偶校验示例
                // 校验失败,重置状态机
                parse_state = DATA_WAIT_SYNC;
                data_index = 0;
            } else {
                parse_state = DATA_READ_ANGLE_L;
            }
            break;
        // ... 依次读取角度低字节、高字节、距离低字节、高字节
        case DATA_READ_DIST_H:
            raw_data_buffer[data_index] = byte;
            // 一个完整数据包接收完毕
            angle = (raw_data_buffer[2] | (raw_data_buffer[3] << 8));
            distance = (raw_data_buffer[4] | (raw_data_buffer[5] << 8));
            parse_state = DATA_PACKAGE_COMPLETE;
            data_index = 0;
            // 此时可以处理或存储angle和distance数据
            ProcessLidarData(angle, distance);
            // 处理完后,回到等待状态,寻找下一帧
            parse_state = DATA_WAIT_SYNC;
            break;
        default:
            parse_state = DATA_WAIT_SYNC;
            data_index = 0;
            break;
    }
}

在主循环中,我们不断查询串口接收标志位,并调用解析函数:

while(1) {
    // 其他任务...

    // 处理雷达数据
    if(USART_GetFlagStatus(USART2, USART_FLAG_RXNE) != RESET) {
        uint8_t received_byte = USART_ReceiveData(USART2);
        LaserRadar_Data_Parser(received_byte);
    }

    // 按键控制示例
    if(Key_Scan() == KEY_ON) {
        if(radar_running) {
            LaserRadar_PWM_Set(0); // 停止PWM
            LaserRadar_Send_Cmd(stop_cmd, STOP_CMD_LEN); // 发送停止指令
            radar_running = 0;
            printf("Radar Stopped.\r\n");
        } else {
            LaserRadar_PWM_Set(900); // 启动PWM
            delay_ms(100); // 等待电机稳定
            LaserRadar_Send_Cmd(scan_cmd, SCAN_CMD_LEN); // 发送开始扫描指令
            radar_running = 1;
            printf("Radar Started.\r\n");
        }
    }
}

4. 系统集成与高级控制策略

将PWM控制与串口通信结合起来,就构成了一个完整的激光雷达驱动系统。但在实际项目中,我们还需要考虑更多细节。

4.1 启动与停止时序优化

一个健壮的启动流程不是简单地同时打开PWM和发送指令。电机从静止到额定转速需要时间。一个更好的实践是:

  1. PWM软启动:逐步增加PWM占空比(例如每10ms增加10%),直至目标值,以减少启动电流。
    void LaserRadar_Soft_Start(u16 target_compare) {
        for(u16 i=0; i<target_compare; i+=100) { // 步进增加
            TIM_SetCompare1(TIM2, i);
            delay_ms(10); // 每步间隔
        }
        TIM_SetCompare1(TIM2, target_compare); // 达到目标
    }
    
  2. 延迟发送指令:在PWM达到稳定转速后(例如延时200-500ms),再发送串口开始扫描指令,确保雷达内部系统已就绪。
  3. 停止流程:先发送停止扫描指令,等待雷达停止数据输出,再逐步降低PWM占空比至零,实现软停止。

4.2 错误处理与状态监控

一个工业级的产品需要鲁棒性。我们可以增加以下机制:

  • 电机堵转检测:通过ADC监测电机驱动电路的电流。如果电流持续超过阈值而转速(可通过雷达数据反馈间接判断)未提升,可能发生堵转,应立即关闭PWM并报警。
  • 通信超时:如果长时间(如2秒)未收到任何有效的雷达数据帧,应判定通信异常,尝试复位通信或进入安全状态。
  • 看门狗:启用独立看门狗(IWDG),防止程序跑飞导致电机失控。

4.3 资源管理与低功耗设计

对于电池供电的设备,功耗至关重要。

  • 完全休眠:当不需要雷达时,不仅将PWM设为0,还可以关闭(TIM_Cmd(DISABLE))定时器时钟,将控制GPIO设为模拟输入以降低功耗,并让雷达进入深度休眠模式(可能需要发送特定串口指令)。
  • 间歇工作:对于某些应用场景,可以周期性地启动雷达扫描几秒钟,采集环境信息后立即休眠,从而大幅延长续航。

通过将PWM的精准硬件控制与串口的灵活指令交互相结合,STM32C8T6能够为激光雷达提供一个稳定、可靠且功能丰富的驱动平台。从理解电机驱动原理,到配置定时器生成合适的PWM波形,再到设计稳健的串口通信协议和系统状态机,每一步都需要仔细考量实际硬件特性和应用需求。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐