第二章 硬件接口基础: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主要干两件事:
- 控制直流电机速度:占空比越大,平均电压越高,电机转得越快。
- 控制舵机角度: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是发令枪,编码器是测量尺,中断是警报铃。五者配合,才能让电机听话地转起来。
我的建议:初学者不要急着把所有接口都用上。先拿一个LED练GPIO输出,再拿一个电位器练ADC输入,然后拿一个舵机练PWM。一步步来,每个接口都亲手调通,后面做复杂项目时才不会手忙脚乱。
嗯,这一章的内容就到这里。硬件接口是运动控制的「地基」,地基不牢,楼盖得再高也得塌。下一章我们会把这些接口组合起来,真正让电机转起来。到时候,你会看到这些「零件」是怎么拼成一台机器的。
公众号:蓝海资料掘金营,微信deep3321