第二章 硬件接口基础:GPIO原理与编程、PWM信号生成、编码器接口、中断处理机制

各位同学,大家好。欢迎来到《C语言运动控制编程实战》的第二讲。

上一章我们聊了运动控制的整体框架,说白了就是「大脑怎么指挥手脚」。今天我们要深入底层,看看大脑和手脚之间的「神经末梢」——硬件接口。

你想想看,电机不会直接听懂C语言的if-else。它只认电平高低、脉冲宽度、边沿跳变。所以,搞运动控制的第一步,就是学会跟这些硬件引脚「对话」。

我个人习惯把硬件接口比作「四把钥匙」:GPIO、PWM、编码器、中断。今天咱们一把一把地讲透。

2.1 GPIO:最基础的「开关」与「眼睛」

GPIO,全称通用输入输出口。说白了,就是芯片上那些可以编程控制的引脚。

你可以把它想象成一个智能开关:

  • 输出模式:你写1,引脚输出高电平(通常是3.3V或5V);你写0,引脚输出低电平(0V)。
  • 输入模式:你读取引脚上的电平,是1还是0。

在运动控制里,GPIO最常见的用途就是控制继电器的通断、读取限位开关的状态、或者给步进电机驱动器发脉冲方向信号。

核心要点:GPIO的编程,本质上就是操作寄存器。你往某个地址写数据,引脚就变了;你从某个地址读数据,就知道外部状态了。

来看一个典型的GPIO初始化代码(以STM32为例):

// 初始化PA0为推挽输出,PA1为上拉输入
void GPIO_Init(void)
{
    RCC->AHB1ENR |= (1 << 0);      // 使能GPIOA时钟,这一步容易忘!
    
    // PA0: 输出模式
    GPIOA->MODER &= ~(0x3 << (0 * 2));  // 先清零
    GPIOA->MODER |= (0x1 << (0 * 2));   // 设为输出模式
    
    // PA1: 输入模式,带上拉
    GPIOA->MODER &= ~(0x3 << (1 * 2));  // 清零,默认就是输入
    GPIOA->PUPDR |= (0x1 << (1 * 2));   // 上拉电阻使能
}

个人经验:我刚开始做项目时,经常忘记使能GPIO时钟。结果代码怎么调都不对,引脚死活没反应。后来养成了习惯——初始化任何外设,第一件事就是开时钟。你记住这个顺序:时钟 → 模式 → 速度/上下拉。

2.2 PWM信号生成:让电机「动」起来

PWM,脉冲宽度调制。说白了,就是通过调节方波的占空比,来模拟一个「可变的电压」。

为什么不用真正的模拟电压?因为数字电路处理高低电平最拿手,模拟信号反而容易受干扰。PWM的本质是「用时间换精度」。

在运动控制里,PWM主要干两件事:

  1. 控制直流电机速度:占空比越大,平均电压越高,电机转得越快。
  2. 控制舵机角度:20ms周期,1ms脉宽对应0°,1.5ms对应90°,2ms对应180°。

来看一个用定时器生成PWM的例子:

// 配置TIM2的CH1输出PWM,频率1kHz,占空比50%
void PWM_Init(void)
{
    RCC->APB1ENR |= (1 << 0);      // 使能TIM2时钟
    
    // 配置GPIO为复用功能(PA0对应TIM2_CH1)
    GPIOA->MODER &= ~(0x3 << (0 * 2));
    GPIOA->MODER |= (0x2 << (0 * 2));  // 复用功能模式
    GPIOA->AFR[0] |= (0x1 << (0 * 4)); // AF1对应TIM2
    
    // 配置定时器
    TIM2->PSC = 83;                   // 预分频,84MHz / (83+1) = 1MHz
    TIM2->ARR = 999;                  // 自动重装,1MHz / (999+1) = 1kHz
    TIM2->CCR1 = 500;                 // 占空比50%
    
    TIM2->CCMR1 |= (0x6 << 4);       // PWM模式1
    TIM2->CCER |= (1 << 0);         // 使能CH1输出
    TIM2->CR1 |= (1 << 0);          // 启动定时器
}

避坑指南:我曾经在一个项目中,PWM输出一直不对,波形乱跳。查了半天,发现是GPIO的复用功能配置错了——引脚功能没映射到正确的定时器通道。记住:PWM不是GPIO的「普通输出」,必须配置为「复用功能」,并且选对AF编号。

2.3 编码器接口:知道电机「转了多少」

开环控制只能「发指令」,闭环控制需要「看反馈」。编码器就是那个反馈眼睛。

最常见的增量式编码器,输出两路正交信号:A相和B相。电机正转时,A领先B 90°;反转时,B领先A 90°。

通过检测这两路信号的边沿,我们可以知道:

  • 位置:累计了多少个脉冲
  • 方向:A和B谁先跳变
  • 速度:单位时间内的脉冲数

很多MCU都内置了编码器接口模式,直接用定时器就能解算。来看配置:

// 配置TIM3为编码器模式,PA6(TIM3_CH1)接A相,PA7(TIM3_CH2)接B相
void Encoder_Init(void)
{
    RCC->APB1ENR |= (1 << 1);      // 使能TIM3时钟
    
    // GPIO配置为复用功能
    GPIOA->MODER &= ~(0x3 << (6 * 2)) & ~(0x3 << (7 * 2));
    GPIOA->MODER |= (0x2 << (6 * 2)) | (0x2 << (7 * 2));
    GPIOA->AFR[0] |= (0x2 << (6 * 4)) | (0x2 << (7 * 4)); // AF2
    
    // 定时器配置
    TIM3->PSC = 0;                    // 不分频
    TIM3->ARR = 0xFFFF;               // 最大计数值
    
    TIM3->CCMR1 |= (0x3 << 0);       // 输入捕获,CC1映射到TI1
    TIM3->CCMR1 |= (0x3 << 8);       // CC2映射到TI2
    TIM3->SMCR |= (0x3 << 0);        // 编码器模式3(TI1和TI2都计数)
    TIM3->CR1 |= (1 << 0);           // 启动
}

// 读取编码器计数值
int16_t Encoder_GetCount(void)
{
    return (int16_t)TIM3->CNT;        // 有符号数,正转增加,反转减少
}

核心要点:编码器模式3下,每个边沿都计数,分辨率是编码器线数的4倍。比如一个1000线的编码器,一圈能产生4000个脉冲。这个细节很多人容易忽略。

2.4 中断处理机制:让CPU「及时响应」

轮询方式太浪费CPU了——你总不能一直盯着一个引脚看它变了没。中断就是那个「来了活再叫我」的机制。

在运动控制里,中断的典型场景:

  • 外部中断:限位开关被触发,立即停止电机
  • 定时器中断:每隔固定时间执行一次PID计算
  • 编码器中断:每转一圈,触发一次位置校准

来看一个外部中断的配置:

// 配置PA1为下降沿触发的外部中断(接限位开关)
void EXTI_Init(void)
{
    RCC->APB2ENR |= (1 << 14);     // 使能SYSCFG时钟
    
    // 配置GPIO
    GPIOA->MODER &= ~(0x3 << (1 * 2));  // 输入模式
    
    // 连接EXTI1到PA1
    SYSCFG->EXTICR[0] &= ~(0xF << (1 * 4));
    SYSCFG->EXTICR[0] |= (0x0 << (1 * 4)); // PA口
    
    // 配置中断触发方式
    EXTI->IMR |= (1 << 1);          // 不屏蔽中断线1
    EXTI->FTSR |= (1 << 1);         // 下降沿触发
    
    // 配置NVIC
    NVIC_EnableIRQ(EXTI1_IRQn);
    NVIC_SetPriority(EXTI1_IRQn, 1); // 优先级设高一点
}

// 中断服务函数
void EXTI1_IRQHandler(void)
{
    if(EXTI->PR & (1 << 1))         // 检查中断标志
    {
        EXTI->PR = (1 << 1);        // 清除标志,这一步必须做!
        
        // 紧急停止电机
        Motor_Stop();
        
        // 记录故障信息
        fault_flag = 1;
    }
}

避坑指南:我曾经犯过一个低级错误——在中断服务函数里调用了printf。结果程序一跑就死机。为什么?printf太慢了,而且可能引起重入问题。记住:中断服务函数要「短平快」,只做最必要的事。复杂逻辑放到主循环里处理。

2.5 知识体系总览

好了,四把钥匙都讲完了。我画了一张图,帮你理清它们之间的关系:

运动控制硬件接口知识体系 MCU 核心 GPIO 接口 PWM 信号 编码器接口 中断处理 运动控制应用(电机驱动/位置闭环) 开关量输入/输出 限位开关、继电器 模拟量替代方案 电机速度/舵机角度 位置/速度反馈 增量式/绝对式 实时响应机制 紧急停止/定时控制

这张图你看懂了吗?MCU是大脑,GPIO是手和眼睛,PWM是发令枪,编码器是测量尺,中断是警报铃。五者配合,才能让电机听话地转起来。

我的建议:初学者不要急着把所有接口都用上。先拿一个LED练GPIO输出,再拿一个电位器练ADC输入,然后拿一个舵机练PWM。一步步来,每个接口都亲手调通,后面做复杂项目时才不会手忙脚乱。

嗯,这一章的内容就到这里。硬件接口是运动控制的「地基」,地基不牢,楼盖得再高也得塌。下一章我们会把这些接口组合起来,真正让电机转起来。到时候,你会看到这些「零件」是怎么拼成一台机器的。


公众号:蓝海资料掘金营,微信deep3321