news 2026/11/6 4:33:32

STM32F103C8T6+HC05+L298N蓝牙小车:从接线到调速的完整避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103C8T6+HC05+L298N蓝牙小车:从接线到调速的完整避坑指南

简介:这是一份基于STM32F103C8T6的蓝牙遥控小车课程设计资源,面向刚接触嵌入式、需要完成课设或入门练手的学生。项目通过HC05蓝牙模块接收指令,由L298N电机驱动模块控制小车前进、后退、左转与右转,功能虽基础但流程完整,适合作为STM32外设入门与电机控制的参考案例。压缩包共60个文件,约1.59MB,包含c与h源码、o与crf编译中间文件、uvprojx与uvoptx工程配置、map与lst链接列表、axf可执行文件及接线示意图等,覆盖从源码到编译产物的完整工程结构。目前已有6138人学习下载。读者可从中获取蓝牙串口通信、GPIO控制、电机驱动逻辑与Keil工程搭建的完整实现,并借助接线图快速复现硬件连接,理解L298N与STM32的配合方式,为后续扩展循迹、调速等功能提供可运行的基础框架。

1. 从一堆散件到能跑的车:stm32f103c8t6+hc05+l298n蓝牙小车到底难在哪

很多人第一次做 stm32f103c8t6+hc05+l298n 蓝牙小车,卡住的地方根本不是写代码,而是把这三块东西凑到一起之后,车要么不动,要么只会原地打转,要么手机一连上蓝牙,单片机就复位。我见过太多人把 L298N 的使能跳帽一插,以为接线对了,结果 PWM 根本没输出,电机只是嗡嗡响。这套方案的核心其实就三件事:STM32F103C8T6 出 PWM 控制 L298N 的两路使能和四个方向脚,HC05 通过 UART 收手机指令,主循环把指令翻译成左右轮的速度和方向。它适合已经点过灯、串口能收发、但还没把“通信+电机+电源”三条线理清楚的人。把这套跑通,你后面换编码器、换 PID、换 ROS 都是同一套骨架。

2. 先定引脚再焊线:STM32F103C8T6 与 L298N 的接线和 PWM 分配

2.1 为什么选 TIM 通道而不是随便 GPIO 拉高拉低

L298N 的 ENA、ENB 是使能脚,本质上控制的是电机平均电压。如果你只用 GPIO 拉高,电机永远全速,蓝牙小车就变成“开关车”,没法调速,转弯也会因为两侧轮速一样而画大圈。STM32F103C8T6 的 TIM1、TIM2、TIM3、TIM4 都能出 PWM,但 TIM1 是高级定时器,配置起来多一步主输出使能,新手容易在这里翻车。我一般把左轮使能放 PA0(TIM2_CH1),右轮使能放 PA1(TIM2_CH2),这样两路 PWM 同频,占空比独立,代码里只需要一个定时器初始化。

方向脚选普通 GPIO 就行,但要注意避开 JTAG 占用的 PA13、PA14、PA15、PB3、PB4。常见分配是:左轮 IN1 接 PB12,IN2 接 PB13,右轮 IN3 接 PB14,IN4 接 PB15。这样 TIM2 管速度,GPIOB 管方向,互不打架。

2.2 定时器参数怎么算才不玄学

PWM 频率不能太低,太低电机会听到啸叫;也不能太高,L298N 内部达林顿管跟不上。我一般把频率设在 10 kHz 到 20 kHz 之间,取 16 kHz 左右比较稳。STM32F103C8T6 系统主频 72 MHz,TIM2 挂在 APB1 上,时钟也是 72 MHz。分频和重装载值这样算:

// TIM2 PWM 初始化,目标频率 16kHz,占空比分辨率 1000 级 // 72MHz / (71+1) = 1MHz 计数时钟 // 1MHz / (62+1) ≈ 15.87kHz,接近 16kHz // 占空比范围 0~62,实际用 0~62 映射 0~100% void TIM2_PWM_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽,PWM 必须用这个 gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler = 71; // 72MHz / 72 = 1MHz tim.TIM_Period = 62; // 1MHz / 63 ≈ 15.87kHz tim.TIM_CounterMode = TIM_CounterMode_Up; tim.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tim); TIM_OCInitTypeDef oc; oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = 0; // 初始占空比 0,电机不转 oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &oc); TIM_OC2Init(TIM2, &oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); }

这段代码里TIM_Prescaler = 71和TIM_Period = 62是配对使用的,改一个就要重算另一个。TIM_Pulse就是比较值,写 0 停转,写 62 全速。注意 GPIO 模式必须是GPIO_Mode_AF_PP,配成普通推挽输出是没有 PWM 波形的,这是新手最常踩的坑之一。

2.3 方向控制表要背下来

L298N 每路两个输入脚,电平组合决定正转、反转、刹车、停止。我习惯在代码里写一个宏表,避免每次现推:

左轮 IN1左轮 IN2左轮状态右轮 IN3右轮 IN4右轮状态
10正转10正转
01反转01反转
00停止00停止
11刹车11刹车

前进就是左右都正转,后退都反转,左转是左轮停或反转、右轮正转,右转反过来。原地旋转就是一侧正转一侧反转。这张表建议直接抄进头文件,用#define写清楚,后面调蓝牙指令时直接调用。

3. HC05 串口收指令:从手机字节到电机动作的完整链路

3.1 HC05 的默认参数和 AT 模式进入条件

HC05 出厂一般是 9600 波特率、8 数据位、1 停止位、无校验。上电后如果 KEY 脚悬空或低电平,它处于通信模式;KEY 接高电平再上电,才进 AT 模式。很多人 AT 指令发不进去,就是因为 KEY 没拉高就上电了。通信模式下,HC05 的 TXD 接 STM32 的 RX,RXD 接 STM32 的 TX,交叉接法是血泪经验,接成同名对同名永远收不到。

STM32F103C8T6 我用 USART1,PA9 是 TX,PA10 是 RX。初始化时波特率必须和 HC05 一致,否则收到的全是乱码。中断接收比轮询靠谱,因为主循环还要跑 PWM 更新,轮询容易丢字节。

// USART1 初始化,9600 波特率,开启接收中断 void USART1_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio; gpio.GPIO_Pin = GPIO_Pin_9; // TX gpio.GPIO_Mode = GPIO_Mode_AF_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_10; // RX gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &gpio); USART_InitTypeDef usart; usart.USART_BaudRate = 9600; usart.USART_WordLength = USART_WordLength_8b; usart.USART_StopBits = USART_StopBits_1; usart.USART_Parity = USART_Parity_No; usart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; usart.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, &usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART1_IRQn); USART_Cmd(USART1, ENABLE); } // 中断服务函数,收到一个字节就存进缓冲区 volatile uint8_t rx_buf[32]; volatile uint8_t rx_len = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { uint8_t ch = USART_ReceiveData(USART1); if (rx_len < sizeof(rx_buf) - 1) { rx_buf[rx_len++] = ch; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }

USART_IT_RXNE是接收非空中断,每来一个字节触发一次。rx_buf和rx_len用volatile修饰,因为主循环和中断都会访问,不加编译器可能优化掉。缓冲区开 32 字节够用了,手机端一般发单字符指令,比如F、B、L、R、S。

3.2 指令解析:单字符协议最稳

手机端用蓝牙串口助手,按键发单字符,是最不容易出错的方案。协议越简单,调试越快。我一般定义:

  • F前进
  • B后退
  • L左转
  • R右转
  • S停止
  • 1到9调速,对应占空比 10% 到 90%

主循环里检查rx_len,有数据就取出来解析,解析完把rx_len清零。注意不要在中断里做电机控制,中断里只收数据,动作放主循环,否则容易因为中断嵌套导致 PWM 更新异常。

// 主循环中的指令处理 void Process_Command(void) { if (rx_len == 0) return; uint8_t cmd = rx_buf[0]; rx_len = 0; // 清空,只取第一个字节 switch (cmd) { case 'F': Motor_Set(1, 0, 1, 0); // 左正转,右正转 PWM_Set(50, 50); // 默认 50% 速度 break; case 'B': Motor_Set(0, 1, 0, 1); PWM_Set(50, 50); break; case 'L': Motor_Set(0, 0, 1, 0); // 左停,右正转 PWM_Set(0, 50); break; case 'R': Motor_Set(1, 0, 0, 0); PWM_Set(50, 0); break; case 'S': Motor_Set(0, 0, 0, 0); PWM_Set(0, 0); break; default: if (cmd >= '1' && cmd <= '9') { uint8_t speed = (cmd - '0') * 10; PWM_Set(speed, speed); } break; } }

Motor_Set负责写四个方向 GPIO,PWM_Set负责写 TIM2 的比较寄存器。这两个函数分开写,后面加 PID 时只改PWM_Set的入参就行,方向逻辑不用动。

3.3 电源部分:别让电机把单片机拖复位

L298N 的 12V 输入接电池,5V 输出可以给 HC05 供电,但不要用它给 STM32F103C8T6 最小系统板供电。电机启动瞬间电流能到 1A 以上,L298N 的 5V 输出会掉压,STM32 跟着复位,表现就是“蓝牙一连上,车就重启”。正确做法是:电池分两路,一路进 L298N 的 12V,一路进降压模块降到 5V 或 3.3V 给 STM32 和 HC05。共地必须接,不共地串口收不到数据。

4. 避坑与排查:蓝牙小车不动的 5 个真实原因

4.1 现象:手机显示已连接,但发指令没反应

原因通常是 HC05 的 RXD 和 TXD 接反了,或者 STM32 的 USART 中断没开。先拿示波器看 PA10 有没有波形,没有就是接线问题;有波形但rx_len一直是 0,检查NVIC_EnableIRQ和USART_ITConfig是否都调用了。还有一种情况是手机端发了带换行符的字符串,rx_buf[0]是有效字符,但后面跟了\r\n,如果代码里判断了长度就会漏掉。

4.2 现象:电机嗡嗡响但不转

L298N 的使能跳帽没拔,ENA、ENB 被强制拉高,PWM 进不去。或者 PWM 频率太低,电机听到的是断续电流。把跳帽拔掉,用杜邦线接 PA0、PA1,频率按第 2 章算到 16 kHz 左右。另外检查电池电压,低于 7V 时 L298N 内部压降太大,电机也转不动。

4.3 现象:小车只往一个方向跑,换指令没用

方向脚接错了,或者Motor_Set里 GPIO 写反。拿万用表量 IN1 到 IN4 的电平,对照第 2 章的方向表逐行核对。还有一种可能是 L298N 某一路已经烧了,交换左右电机插头,如果故障跟着电机走就是电机问题,跟着 L298N 走就是驱动板问题。

4.4 现象:调速时电机抖动或突然停

TIM_Pulse写入了超过TIM_Period的值。比如周期是 62,你写了 100,比较值永远大于计数值,输出恒高或恒低。调速映射时把百分比转成 0 到 62 之间的整数,别直接写 0 到 100。另外TIM_OCPreload_Enable没开的话,改比较值会在计数器溢出时才生效,表现就是调速有延迟。

4.5 现象:跑几分钟后单片机发热或复位

L298N 的散热片烫手是正常的,但 STM32 发热不正常。检查 5V 降压模块输出是否稳定,电机电源和逻辑电源有没有隔离。我一般会在 L298N 的 12V 输入端并一个 1000uF 电解电容,吸收电机启停时的反向电动势。HC05 的电源脚也并一个 0.1uF 瓷片电容,防止射频干扰串进电源。

5. 进阶技巧:用定时器中断做速度斜坡和指令超时保护

5.1 速度斜坡:让启停不顿挫

直接写PWM_Set(50, 50)时,电机会瞬间从 0 跳到 50%,齿轮箱冲击很大,小车会“点头”。我一般用 TIM3 做一个 10ms 中断,在中断里让当前速度逐步逼近目标速度,每次加 1 或减 1。这样从 0 到 50% 大约需要 500ms,起步平顺,停止也不甩尾。

// TIM3 初始化,10ms 中断一次 void TIM3_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_TimeBaseInitTypeDef tim; tim.TIM_Prescaler = 7199; // 72MHz / 7200 = 10kHz tim.TIM_Period = 99; // 10kHz / 100 = 100Hz,即 10ms tim.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tim); TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM3_IRQn); TIM_Cmd(TIM3, ENABLE); } volatile int16_t cur_left = 0, cur_right = 0; volatile int16_t tgt_left = 0, tgt_right = 0; void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) { // 左轮斜坡 if (cur_left < tgt_left) cur_left++; else if (cur_left > tgt_left) cur_left--; // 右轮斜坡 if (cur_right < tgt_right) cur_right++; else if (cur_right > tgt_right) cur_right--; TIM_SetCompare1(TIM2, cur_left); TIM_SetCompare2(TIM2, cur_right); TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }

cur_left和cur_right是实际输出,tgt_left和tgt_right是目标值。蓝牙指令只改目标值,中断负责平滑过渡。注意TIM_SetCompare1写的是比较值,范围 0 到 62,所以tgt_left最大也只能给 62。

5.2 指令超时保护:断连就停车

蓝牙链路偶尔会断,如果手机端不发了,小车会保持最后的速度一直跑,这是很危险的。我在 TIM3 中断里加一个计数器,每收到一次指令清零,超过 500ms 没收到就把目标速度设为 0。

volatile uint16_t cmd_timeout = 0; // 在 Process_Command 里收到有效指令后 cmd_timeout = 0; // 在 TIM3_IRQHandler 里每 10ms 加一 if (cmd_timeout < 1000) cmd_timeout++; if (cmd_timeout > 50) // 50 * 10ms = 500ms { tgt_left = 0; tgt_right = 0; }

这个逻辑简单但有效,实测在蓝牙信号弱的环境下能避免小车失控。cmd_timeout上限设 1000 是防止溢出,实际判断用 50 就够了。

5.3 验证方法:先架空轮子再落地

调车之前把小车架起来,让轮子悬空。发F看两个轮子是否同向同速,发L看左轮是否停或反转。用手机秒表测一下从发指令到轮子动的时间,正常应该在 100ms 以内。如果超过 500ms,检查蓝牙模块的波特率是否匹配,或者主循环里有没有阻塞延时。架空测试没问题再落地,能省很多追车的时间。

我自己的习惯是每次改完代码先架空跑一遍,确认方向、调速、超时都正常再放地上。这个习惯帮我省了至少三次撞墙后的重新焊接。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 4:33:24

NPU/TPU深度学习硬件加速解析:架构差异、部署流程与性能调优

搞深度学习的人大都体会过什么叫“算力焦虑”。模型从几十层卷积一路涨到几百B参数&#xff0c;通用芯片跑起来又热又慢&#xff0c;成本还压不住。于是NPU和TPU这类硬件加速单元成了绕不开的话题。我第一次接触NPU&#xff0c;是接手一个要把图像分类模型塞进嵌入式板卡的活儿…

作者头像 李华
网站建设 2026/10/26 6:03:33

Scaling Law实战指南:大模型训练中参数量与数据量的最优配比

1. 为什么参数量翻倍&#xff0c;效果却没有线性增长很多人第一次接触大语言模型训练时&#xff0c;都会有一个朴素的想法&#xff1a;参数越多&#xff0c;模型越聪明&#xff0c;效果越好。这个直觉在早期小模型时代确实成立&#xff0c;但当你真正把参数量从十亿推到百亿、千…

作者头像 李华
网站建设 2026/10/22 7:45:06

FP8量化与长上下文部署实战:精度崩塌点与推理框架选型

1. 从权重到激活&#xff1a;FP8 量化到底动了哪些东西很多人第一次接触 FP8&#xff0c;脑子里冒出来的第一个念头是“把模型权重从 16 位压到 8 位&#xff0c;显存直接砍半”。这个理解只对了一半。真正跑过一轮完整量化流程的人会发现&#xff0c;权重压缩只是开胃菜&#…

作者头像 李华
网站建设 2026/10/20 3:41:03

REA模型重构进销存系统:从表单驱动到事件驱动建模实战

接手某配件公司的进销存系统重构时&#xff0c;最让我头疼的不是并发扣库存&#xff0c;也不是报表出数慢&#xff0c;而是“业务规则一变&#xff0c;数据库结构就要跟着动”。三个月内&#xff0c;业务先后要求把采购溢短装按合理误差处理、把销售包装费和运费拆开结算、让成…

作者头像 李华
网站建设 2026/10/21 5:58:46

张宇1000题几何应用分水岭:核心考点与刷题避坑指南

1. 为什么“几何应用”这一章是很多人的分水岭如果你正在用张宇1000题刷高等数学&#xff0c;翻到第五章“一元函数微分学的应用&#xff08;一&#xff09;——几何应用”的时候&#xff0c;大概率会有两种反应&#xff1a;一种是觉得前面极限、导数都扛过来了&#xff0c;这章…

作者头像 李华