公司动态
STM32串口控制TB6612驱动直流电机转速实战
简介基于STM32与TB6612的串口控制直流电机转速项目资料包主要面向嵌入式初学者、电机控制方向的在校学生及电子竞赛备赛者。资源围绕UART串口通信与PWM调速技术展开内容涵盖STM32F4标准外设库的初始化流程、直流电机转速与电压关系、TB6612驱动芯片的引脚时序以及串口指令解析与控制策略。压缩包共249个文件包含STM32F4系列定时器、复位、ADC等外设驱动源码以及编译生成的.o/.axf/.hex文件、Keil MDK工程配置文件和PDF说明文档整体仅1.39MB。已有751人学习下载资料紧凑完整可直接编译运行。工程中实现UART波特率、数据位、校验位等参数配置并利用定时器产生PWM信号调节电机电压通过串口发送命令即可改变转速。同时包含GPIO控制、故障保护等关键模块适合作为课程设计、毕业设计或入门实战的完整参考。 先把这个项目定性这是一个典型的嵌入式入门到进阶的“小闭环”实验——上位机通过串口发指令单片机解析指令后用PWM信号驱动TB6612模块最终控制直流电机的转速。类似的项目在课设、竞赛、个人DIY里出镜率极高但很多人在复现的时候卡在“能转”和“转得稳”之间这篇文章就把从头到尾的坑和细节摊开讲清楚。我拿到的是压缩包文件“8.串口控制直流电机转速TB6612电机驱动.7z”解压之后就是完整的工程源码、原理图还有使用说明后面的所有内容都围绕这套工程展开。这套方案非常适合刚学完STM32基础外设、想串一个完整链路的同学也适合做小车、做云台、做小型自动化设备时需要一套稳的调速方案的人。1. 项目全景与技术选型1.1 这个项目到底解决了什么问题很多人第一次接触电机控制是从“GPIO翻转电平让它转”开始的。但实际应用场景里电机要么转要么停远远不够你需要的是“让它以每小时多少转的速度稳定转”或者“让它根据传感器数据实时调整速度”。这就引出了两个核心需求一是要能人为设定目标转速二是要能让转速尽量贴近目标值。串口控制直流电机转速解决的正是第一个需求——给用户一个方便、高效、可扩展的调速接口。你看市面上所有正经的电机控制器无论是商业的驱动器还是开源的ODrive默认标配都是串口/CAN通信。按键调速和电位器调速只适合现场手动操作一旦你要做自动化让上位机或者传感器根据条件自动调转速串口几乎是性价比最高的选择。硬件上你只占一个UART外设代码上只需要一个结构体存目标值剩下的计算和处理全丢给单片机这个思路会被后面所有的项目继续复用。1.2 为什么选串口加TB6612这条技术路线先看驱动芯片。市面上常用的电机驱动方案有三类L298N、TB6612、分立MOS管H桥。L298N是老古董了压降大两个管压降加起来奔着2V以上去了发热严重而且逻辑电平还不兼容3.3V单片机需要额外的电平转换。分立MOS管H桥性能最好但需要自己搭栅极驱动、死区电路、防共态导通保护对新手来说门槛太高调试周期长。TB6612就是那个“中间项”。它内部集成了两个完整的H桥逻辑输入直接兼容3.3V可以直接接STM32的GPIO不需要电平转换芯片。输出电流在平均1.2A、峰值3.2A的水平驱动常见的N20减速电机、370电机、TT马达都够用。更重要的是这个芯片自带续流二极管、过流保护电路外围只需要几个滤波电容不用自己折腾烧脑的保护电路。在这里选它主要图的就是“开发效率优先性能够用就好”。如果你后续要做大功率电机几十瓦以上再考虑换大电流驱动器或者分立MOS方案。串口通信这块UART是最经典的选择。I2C和SPI虽然速度更快但四线制SCL/SDA/CS等接起来麻烦调试也不直观。你写一个串口调试助手发个字符串就完事了逻辑非常透。USB转串口芯片用CH340就够了Windows自动识别驱动硬件成本几块钱新手折腾起来没压力。2. 硬件接线与TB6612驱动原理2.1 TB6612模块的核心引脚与内部结构TB6612模块拿出来你第一眼看到的是一排排针。有些是逻辑引脚有些是电源引脚有些是电机引脚。如果你不完全清楚每个引脚是干什么的后面接线一定会乱。先花两分钟把引脚功能理顺。从功能上分TB6612FNG芯片内部就是两个独立H桥对应两颗电机每个H桥需要三路信号控制一条PWM调速信号PWMA两条方向信号AIN1、AIN2。还有一个STBY总使能引脚——这个极其关键很多新手忘了拉高STBY结果芯片完全不工作PWM再怎么发都没用。H桥的原理可以用一个很直观的比喻来理解就像四个开关把电机夹在中间。开关要么让电流从A端流到B端正转要么从B流到A端反转。控制H桥的导通方向就控制了电机旋转方向。调节开关切换的速度即PWM的占空比就调节了平均电压从而调节了转速。TB6612把这四个开关集成到一个芯片里还用MOS管替代了传统H桥的三极管压降小、发热少效率高得多。2.2 完整接线清单以STM32F103C8T6为例下面给出我和这套工程采用的接线方式你可以直接照抄。控制板用的是最常见的STM32F103C8T6“蓝丸”最小系统板PWM选用定时器TIM3的通道1和通道2串口选用USART1具体引脚关系如下表功能TB6612引脚STM32引脚备注电机A方向AIN1PA4普通GPIO输出高低电平控制方向电机A方向AIN2PA5普通GPIO输出与AIN1配合电机A调速PWMAPA6TIM3_CH1输出PWM电机B方向BIN1PA7普通GPIO输出电机B方向BIN2PB0普通GPIO输出电机B调速PWMBPB1TIM3_CH2输出PWM总使能STBYPB2必须拉高否则模块不工作逻辑电源VCC3.3V给芯片逻辑部分供电电机电源VM5V电机供电可从板子5V引脚取或外接电源公共地GNDGND必须共地否则通信不工作有两点特别提醒。第一VM和VCC不是同一个电源VM是电机电源VCC是逻辑电源。很多模块上VCC支持2.7V到5.5V可以直接接单片机的3.3V或5V但VM要接到电机的供电轨上一般5V到12V。如果你给电机用单独电源电源的负极必须和单片机的GND连在一起这是整个系统稳定的前提不共地你会看到转速乱跳、串口乱码这些莫名其妙的问题。第二如果使用外接电源给VM供电建议在VM和GND之间并联一个100uF到470uF的电解电容电机启动瞬间电流非常大不加电容会把电源电压拉垮严重的时候会直接让单片机复位。2.3 电机方向和刹车逻辑TB6612的控制逻辑说白了就是一个真值表。把AIN1和AIN2的状态整理一下你会发现所有的可能性都涵盖了AIN1AIN2电机状态00刹车短接惰性停止01反转10正转11停止放开惯性滑行重点说下“刹车”和“停止”的区别。AIN1AIN20时两个下管同时导通电机两端直接短路相当于用电磁力快速制动适合需要急停的场景。AIN1AIN21时两个上管同时导通电机两端都接正电源电机会因为惯性继续转滑行慢慢停下来。这两个状态在PID调速闭环里很重要——当你需要快速响应目标转速变化时可以用刹车状态辅助减速平时不需要急停就用停止状态。工程代码里我封装了三个函数Motor_SetSpeed、Motor_SetDirection、Motor_Brake实际用起来非常顺手。3. 软件设计与核心代码实现3.1 串口通信协议怎么设计才不乱串口通信本质上就是一个字节一个字节地传但裸传字节没有边界接收端怎么知道“A指令结束、B指令开始”所以必须自己定义一套能区分边界的数据格式。这个工程里采用的就是最简单、最经典也最实用的帧格式帧头数据校验结尾。我定义了一套ASCII字符串协议原因是方便调试直接用串口助手肉眼就能看出来问题。数据段长度说明帧头2字节固定为0xAA 0x55用于检测一帧的开始长度1字节后面数据的字节数命令字1字节0x01设置目标转速0x02设置方向0x03请求当前状态数据N字节转速值的低字节、高字节或其他参数校验1字节前面所有字节的和校验只保留低8位帧尾2字节固定为0x0D 0x0A即\r\n方便调试你可能觉得这个协议有点“过度设计”但实际真不是。如果你只用单个字节传转速比如发送0x80表示“转128”那你想传255是没问题可如果串口多了一个字节比如静电干扰、接触不良整个数据流就会错位后面所有指令全乱了。带帧头帧尾加校验的协议虽然多几个字节但可靠性提升了一个数量级而且这套协议模板以后做任何串口项目都能直接套用属于一次投资终身受益。3.2 STM32串口接收解析的状态机串口数据接收核心问题是“什么时候知道一帧数据收完了”。这里有两种思路一种是定长接收一种是可变长超时判断。我用的是比较通用的状态机法每收到一个字节就根据当前状态去判断它属于哪一段代码结构清晰而且不会因为阻塞在某个while等待接收完成里把系统拖死。状态机有四个状态寻找帧头、接收长度、接收数据、接收校验。收到一个字节就切一个状态逻辑严谨。下面是核心代码完整工程里已在STM32F103上验证typedef enum { STATE_FRAME_HEADER1 0x00, STATE_FRAME_HEADER2, STATE_FRAME_LENGTH, STATE_FRAME_CMD, STATE_FRAME_DATA, STATE_FRAME_CHECK, } UART_RX_STATE; uint8_t rx_buf[64]; uint8_t rx_len 0; uint8_t rx_index 0; uint8_t rx_checksum 0; void UART1_IRQHandler(void) { uint8_t ch 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { ch USART_ReceiveData(USART1); rx_state_machine(ch); } } void rx_state_machine(uint8_t ch) { static uint8_t state STATE_FRAME_HEADER1; switch (state) { case STATE_FRAME_HEADER1: if (ch 0xAA) state STATE_FRAME_HEADER2; else state STATE_FRAME_HEADER1; // 没有帧头就继续找 break; case STATE_FRAME_HEADER2: if (ch 0x55) { state STATE_FRAME_LENGTH; rx_checksum 0xAA 0x55; } else { state STATE_FRAME_HEADER1; } break; case STATE_FRAME_LENGTH: rx_len ch; rx_checksum ch; rx_index 0; if (rx_len 0 rx_len 60) { state STATE_FRAME_CMD; } else { state STATE_FRAME_HEADER1; } break; case STATE_FRAME_CMD: rx_buf[rx_index] ch; rx_checksum ch; state STATE_FRAME_DATA; break; case STATE_FRAME_DATA: rx_buf[rx_index] ch; rx_checksum ch; if (rx_index rx_len - 1) { // 减1是因为包含命令字本身 state STATE_FRAME_CHECK; } break; case STATE_FRAME_CHECK: if (ch rx_checksum) { // 校验通过解析命令 parse_cmd(rx_buf, rx_len); } state STATE_FRAME_HEADER1; break; default: state STATE_FRAME_HEADER1; break; } }注意UART1的中断优先级建议设置为最低或次低因为状态机很轻量不会阻塞太长的时间。如果你同时跑着PWM中断和串口中断一定要理清优先级避免PWM更新频率被串口干扰。另外如果你的单片机主频高还可以用串口DMA空闲中断的收包方式性能更好参数也更好看。但对于这个项目中断收字节完全够用不必为了炫技把复杂度拉上去。3.3 PWM调速与方向控制的定时器配置到了PWM环节需要讲点配置原理。PWM调速的本质是“斩波”调压——频率固定把高电平占一个周期的时间比例调高或调低电机的平均电压就变了转速也就跟着变。真正需要决定的是两个数值频率和占空比。先说占空比这是转速的直接映射。我在代码里把PWM周期设为999ARR999这样占空比计算特别方便CCR的值直接就是“千分比”。比如你要设置50%转速CCR就填500非常直观。再说频率我在工程里用的是10kHz。这个频率选择是有讲究的低于5kHz会有可闻的电机啸叫高于20kHz会显著增加MOS管的开关损耗。10kHz到20kHz之间是比较甜点的区间既能避开人耳敏感频段又能平衡开关损耗。下面是TIM3输出PWM的配置代码生成两个通道的PWM分别控制两个电机void PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // PA6、PA7 复用为TIM3_CH1、TIM3_CH2 GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // 72MHz / 72 1MHz TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseStructure.TIM_Period 1000 - 1; // 1MHz / 1000 1kHz即PWM频率1kHz TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM3, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OC1Init(TIM3, TIM_OCInitStructure); TIM_OC2Init(TIM3, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }上面这段是基础版实际工程里频率用10kHz你把Prescaler改成8-1、Period设为899即9MHz/90010kHz就行。我把Prescaler和Period的计算方式再拆解一下系统主时钟72MHz分频到9MHz预分频8分频再计数到899900个计数周期每个计数周期发出一个PWM脉冲最终频率就是10kHz。如果你要改频率记得公式PWM频率 主时钟频率 / Prescaler1 / Period1不用背现场算就行。方向和转速的联动逻辑我写成一个函数void Motor_SetSpeed(uint8_t motor_id, int16_t speed) { if (motor_id MOTOR_A) { if (speed 0) { AIN1_Level 1; AIN2_Level 0; TIM_SetCompare1(TIM3, speed 1000 ? 1000 : speed); } else if (speed 0) { AIN1_Level 0; AIN2_Level 1; TIM_SetCompare1(TIM3, (-speed) 1000 ? 1000 : (-speed)); } else { AIN1_Level 0; AIN2_Level 0; TIM_SetCompare1(TIM3, 0); } } else { // 同理处理电机B } }为了让代码更健壮我给speed加了限幅保证TIM_SetCompare1的输入值不会超过ARR的值。否则CCR大于ARR时PWM会一直输出高电平看起来是转疯了其实是配置出了问题。3.4 进阶扩展从开环调速到PID闭环控制工程里默认是开环控制——也就是说你发指令设定一个转速单片机就输出对应的PWM占空比电机实际转多快不反馈、不修正。这在空载、轻载条件下基本够用但只要你给电机加上负载比如小车走在粗糙地面上转速就会往下掉开环控制就“失准”了。这时候就需要闭环控制。最简单也最有效的方案是PID。在一个电机调速系统里PID的三个参数各有分工比例项Kp直接反映当前误差误差大就输出大积分项Ki负责消除静态误差让转速最终能精确贴合目标值微分项Kd负责抑制超调让转速不会在目标值附近激烈震荡。硬件上你需要一个转速反馈源最常用的是霍尔编码器。编码器每转一圈输出一定数量的脉冲比如13线编码器通过倍频后能得到52个脉冲/圈单片机通过定时器捕获模式计算单位时间内的脉冲数就换算出实时转速。然后PID算法根据“目标转速”和“当前转速”的差值实时调整PWM占空比。实际做下来编码器加速度环PID转速稳定度能控制在±1%以内这在小车控制和云台控制里已经非常能打了。PID调参的核心口诀是先调Kp让系统响应变快但不震荡再加Ki消除静态误差最后加Kd压住超调。如果你是从零开始建议在串口助手里打印出目标值和实时值做成曲线图边看边调效率比自己瞎猜高得多。Windows下用SerialPlot这类免费的串口绘图工具比用眼睛看数字直观太多了强烈推荐。4. 实操调试与常见问题排查4.1 从烧录到跑通的完整流程拿到这个工程后最快跑通的步骤是这样的安装CH340驱动用USB转TTL模块连接STM32的USART1PA9为TX、PA10为RX交叉连接打开设备管理器确认端口号。打开串口调试助手波特率设成1152008位数据、1位停止位、无校验这个要和代码里的USART_Init配置完全一致不然收到的是乱码。把STM32和TB6612模块按上面表格接好线电机接在模块的A路输出上。给板子供电下载程序可以通过串口ISP下载或者ST-Link SWD下载。在串口助手发送一帧数据AA 55 03 01 D0 07 08 0D 0A。这帧数据的含义是设置电机A的正转目标转速0x07D0即2000转速单位自行定义。如果一切正常电机应该开始转动转速逐渐升高并稳定在2000附近。你可以试着改变转速值再发一次观察电机的响应。首次跑通时建议把所有指示灯都利用起来程序上电时点亮板载LED表示系统初始化成功串口收到一帧有效指令时翻转一次LED状态这样你调试的时候肉眼就能看到串口数据是否被正确接收而不是电机不转但不知道是没收到指令还是指令解析失败。4.2 常见问题速查表我在调试过程中把过去踩过、朋友们踩过的坑都整理了一遍汇总成下面的速查表你遇到问题先对号入座现象可能原因排查方法电机完全不动STBY没拉高检查STBY引脚是否接PB2并拉高到3.3V电机完全不动VM没供电压测量VM引脚和GND之间是否有5-12V电机完全不动串口没收到数据用USB转TTL直接发数据看板载LED是否闪烁电机只朝一个方向转AIN1/AIN2逻辑接反互换两个方向引脚或调整代码里的方向配置电机抖动、转速不稳PWM频率过低检查定时器配置把PWM频率提高到10kHz以上串口显示乱码波特率不匹配检查代码里的波特率和串口助手是否一致串口显示乱码接线太长/干扰缩短杜邦线或用屏蔽线电机转速达不到最大值电源功率不足换大电流电源或用外接电源给VM供电模块发烫电机电流超额定减轻负载或用更大驱动能力的驱动器一启动电机单片机就复位电源被拉垮在VM加电解电容或独立给电机供电4.3 几个值得记住的调试技巧最后分享几个我在实际调试中总结的小技巧。第一个串口助手发送字符串和发送HEX的区别要搞清楚。如果你定义的是二进制协议就必须用HEX模式发送AA和55不能打成ASCII字符“AA55”。字符串协议虽然直观但解析效率稍微低一点点且传中文不方便二进制协议省带宽、速度快但调试时眼睛不那么友好。建议学有余力的话两种协议都实现一下这对你理解数据通信的本质帮助很大。实际工程里我一般会用二进制协议传输实时数据如转速、电流采样用字符串协议传输调试命令两者共存、互不干扰。第二个调试PWM时一定先把方向调到正转确认电机正常转起来再去调速度和PID不要一上来就全上。因为你没法确定是方向问题还是速度问题一次性暴露的变量越多越难定位故障。第三个硬件上给模块的电源和信号线不要走同一束杜邦线特别是电机电流比较大时会产生电磁干扰影响PWM信号的波形质量。如果你的电机是碳刷电机建议在电机两端并联一个小容量瓷片电容104能明显减少换向火花产生的EMI串口乱码的几率也会小很多。第四个PCB布局的排针定义千万别想当然。市面上TB6612模块虽然引脚标注基本一致但有的兼容板会把VM和VCC的顺序颠倒拿到模块第一步是拿万用表通断档打一遍确认每个引脚连接正确再上电不然可能烧板子。这个项目看似简单的“串口控制调速”实际上已经把嵌入式开发里最经典的“上位机通信外设驱动PWM调速”整条链路走了一遍。你把这条链路吃透了后面做PID闭环、做无线遥控、做上位机可视化监控都只是在当前基础上加零件、加模块总体的思路和套路是完全一致的。我自己当年从这条链路走过来最大的感受就是串口没通之前所有调试都是空中楼阁串口一通整个系统就像通了气一样后面每一步都有反馈、都看得见摸得着。希望这篇笔记能帮你尽快走到“通了气”的那一步。本文还有配套的精品资源点击获取