简介面向嵌入式初学者的STM32与Proteus联合仿真智能小车项目以STM32F103C6为主控结合直流电机与PWM调速实现前进、后退及转弯控制可在无实物条件下完成硬件电路搭建与代码验证。压缩包共155个文件约6.08MB包含HAL库C源码、头文件、Keil/MDK工程文件、Proteus仿真工程pdsprj及编译生成的hex/axf文件另附IAR工程备份与核心配置文件便于对照学习工程结构和恢复开发状态。目前已有10231人学习适合希望掌握STM32 GPIO、定时器PWM、电机驱动及Proteus仿真调试流程的开发者。资源覆盖从初始化代码到电机控制逻辑的完整工程框架也包含项目配置文件与工作空间备份可帮助读者快速理解嵌入式小车项目的整体组织方式并在此基础上扩展循迹、避障等功能为后续机器人开发打下基础。1. 需求定位先用Proteus跑通智能小车再决定要不要上实物做智能小车摆在面前的第一问是直接买套件还是先在Proteus里仿真。我的回答很直接先仿真。原因不是省钱这么单薄而是仿真能把调试过程变得可逆——改参数、看波形、重新加载hex三步就能完成一轮验证实物一上电焊死的线、烧掉的引脚、失控的舵机都会让排查成本成倍上升。这套基于STM32及Proteus智能小车仿真方案本质是把「控制逻辑验证」和「硬件成本投入」解耦适合正在做嵌入式课程设计、毕业设计的学生也适合想先验证驱动方案再画板子的开发者。它的核心目标是在纯软件环境里跑出一辆能循迹、能调速、能复现故障的智能小车模型并且每一行代码、每一根连线都可以在答辩时讲清楚。2. 总体架构设计电机驱动、传感器采集与时钟引脚的模块划分智能小车仿真最容易犯的错是一上来就把所有元件拖进画布开始接线。真正常用的做法是先掰开模块把每个模块的职责、供电关系、信号流向写清楚再进Proteus动手。这样到了后面排查故障时才知道该怀疑哪个子电路而不是把整个工程推倒重来。2.1 四个模块的职责划分与数据流按功能可以把整个仿真系统切成四块主控模块、电机驱动模块、传感器采集模块、人机指示模块。主控模块以STM32F103C8作为核心负责产生两路PWM信号、读取两个循迹传感器的电平、执行决策逻辑。电机驱动模块用L298N双H桥芯片接收主控的PWM和方向信号驱动左右两个直流电机。传感器采集模块用两个按键模拟实际循迹传感器的数字输出按下表示检测到黑线释放表示在白底上。人机指示模块用两只LED显示当前左右传感器状态方便答辩现场直接看到输入信号是否到达引脚。数据流是这样的STM32通过定时器通道向L298N的使能端输出PWM方向引脚决定电机正反转传感器电平进入PB0、PB1后主控根据左右轮偏差调整两路PWM占空比形成闭环。供电关系上要注意L298N的逻辑电源与电机电源必须共地否则仿真里波形正常实际跑起来就是玄学问题。模块核心器件Proteus元件名关键信号主控STM32F103C8STM32F103C8PA0/PA1输出PWMPB0/PB1采集传感器电机驱动L298NL298NENA/ENB接PWMIN1~IN4接方向电机直流减速电机模型MOTOR-DCOUT1/OUT2、OUT3/OUT4传感器数字电平输出BUTTON上拉电阻按下低电平模拟压黑线指示LEDLED-RED传感器状态可视化2.2 Proteus仿真电路的搭建步骤画电路图时我习惯按「先主控、再驱动、再采样、最后指示」的顺序放置元件。在Proteus元件库搜索STM32F103C8放置后先把VDD、VDDA、VBAT接到VCC网络VSS、VSSA接到GND这一不做好后面芯片根本不放正。接着搜索L298N它的VS接12V电机电源VSS接5V逻辑电源两个地都接到同一个GND。电机接法要注意左右电机分别接在OUT1/OUT2和OUT3/OUT4上电机的两端不要并接错位。PWM信号从PA0接到L298N的ENAPA1接到ENB方向控制脚PB4~PB7分别接IN1~IN4。占空比通过使能端控制这是L298N的标准用法IN1/IN2决定正反转ENA上的PWM决定平均电压。传感器部分不用做复杂的光电转换电路。在Proteus里做黑线反射检测很别扭直接用按键加一个10k上拉电阻到3.3V按键另一端接地。按下时PB0读到低电平释放时读到高电平。这样做的好处是逻辑和真实红外循迹模块一致——实际电路里比较器输出的也是数字电平只不过这里省掉了运放和电位器答辩时把这个替代逻辑说清楚导师通常不会刁难。2.3 引脚选择与时钟配置背后的原因为什么PWM偏偏选PA0、PA1而不是随意挑两个普通GPIO因为PA0和PA1分别映射到TIM2的通道1和通道2一个定时器就能同时产生两路独立PWM省定时器资源。PA9和PA10不建议用它们是USART1的默认引脚仿真里虽然不冲突但后面如果扩展串口调试就会打架。PB4/PB5默认是JTAG引脚如果要用它们做方向控制需要在初始化里关闭JTAG复用只保留SWD否则电平会被调试接口拉死。这个坑我在实际工程里踩过后面会专门写。时钟配置是另一个容易翻车的地方。STM32F103C8使用内部8MHz RC振荡器时如果代码按外部晶振初始化会让所有定时器周期全部跑偏。仿真里没有实体晶振我就统一走内部时钟HSIPLL倍频到72MHz。定时器挂在APB1总线上要注意APB1分频系数为2时定时器时钟是36MHz的2倍也就是72MHz。熟练掌握这个倍数关系PWM频率计算才不用靠猜。3. 核心实现PWM调速、L298N控制与循迹决策的代码落地仿真电路只是骨架真正让小车动起来的是固件。这一章把代码分成初始化、电机控制接口、循迹决策三部分每一部分都给出能直接抄进工程的关键片段并说明参数为什么要这么取。电机转速不是玄学它就是定时器频率和占空比的数学结果。3.1 初始化时钟、GPIO与定时器PWM的参数计算初始化顺序按时钟→GPIO→定时器展开。先配好72MHz主频再设置引脚复用模式最后初始化TIM2输出PWM。PA0和PA1需要配置成复用推挽输出才能把定时器波形引到引脚上。void TIM2_PWM_Init(uint16_t arr, uint16_t psc) { /* 打开TIM2和GPIOA的时钟 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* PA0/PA1复用为TIM2_CH1/CH2推挽输出 */ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; 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); TIM_OCInitTypeDef TIM_OCInitStructure; TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse 0; /* 初始占空比为0 */ TIM_OC1Init(TIM2, TIM_OCInitStructure); TIM_OC2Init(TIM2, TIM_OCInitStructure); TIM_Cmd(TIM2, ENABLE); }调用时这样写TIM2_PWM_Init(99, 71)。PWM频率 72MHz / (711) / (991) 10kHz。为什么取10kHz而不是1kHzL298N内部的开关管和电机模型在这个频率下工作最顺滑频率太低会听到电机一顿一顿的声音频率太高仿真步长会导致波形失真。占空比分辨率是1/100也就是每1%一档对小车调速足够精细。3.2 电机控制接口方向逻辑与占空比更新的拆分电机控制不能散落在主循环里写要封装成一个接口便于上层逻辑调用。方向引脚用PB4~PB7两两一组控制左右电机正反转。L298N的规则是IN1高、IN2低时正转反过来反转IN1和IN2相同则刹车。void Motor_Control(uint8_t left_dir, uint8_t left_duty, uint8_t right_dir, uint8_t right_duty) { /* 左电机方向IN1PB4, IN2PB5 */ GPIO_WriteBit(GPIOB, GPIO_Pin_4, left_dir ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_5, left_dir ? Bit_RESET : Bit_SET); /* 右电机方向IN3PB6, IN4PB7 */ GPIO_WriteBit(GPIOB, GPIO_Pin_6, right_dir ? Bit_SET : Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_7, right_dir ? Bit_RESET : Bit_SET); /* 占空比写入比较寄存器范围0~99 */ TIM_SetCompare1(TIM2, left_duty); TIM_SetCompare2(TIM2, right_duty); }注意left_duty的范围必须小于等于初始化时的arr值也就是99。如果传入100实际占空比反而变成0这在调试时会非常困惑。我通常会在函数入口加一个限幅判断把传入值钳位到[0, 99]。方向与占空比分开传参的好处是循迹决策层只需要关心「往哪个方向转、转多少」不需要知道底层寄存器怎么操作。3.3 循迹决策双传感器输入与差速回正策略循迹逻辑是整车的判断核心。传感器安装在车头左右两侧黑线在正中时两个传感器都读到高电平车直行。当左传感器压到黑线说明车已经向右偏需要向左修正具体表现是左轮减速或右轮加速。反过来右传感器压线则向右修正。两轮速度差越大回正越快但过大容易摆动实际值要靠仿真反复试。void LineFollower_Task(void) { uint8_t left GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0); uint8_t right GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1); if (left 0 right 0) { /* 两个传感器都压黑线说明进入十字线或停车点减速慢行 */ Motor_Control(1, 40, 1, 40); } else if (left 0) { /* 左偏向左回正左轮加速右轮减速 */ Motor_Control(1, 70, 1, 35); } else if (right 0) { /* 右偏向右回正右轮加速左轮减速 */ Motor_Control(1, 35, 1, 70); } else { /* 全部丢线停车等待 */ Motor_Control(0, 0, 0, 0); } }主循环里以20ms为周期调用这个任务函数相当于50Hz的控制频率。20ms的间隔既能让传感器电平稳定又不会让小车响应过于迟钝。直行速度取60转向时一侧提到70一侧降到35差速大约35%经过仿真验证转弯半径最小且不甩尾。这些参数不是一拍脑袋定的是在Proteus里反复跑直线和弯道场景调出来的。答辩时被问到「为什么是35不是50」可以理直气壮说这是仿真标定结果。4. 常见问题与排查固件加载、电机失控等五个翻车点修复仿真平台和真实硬件的踩坑路径不太一样但更值得写出来。下面这五个问题是Proteus里最常见的基本覆盖了从加载固件到运行失控的全过程。每一条都按「现象、原因、解决」的结构记录也是我当初一步步排查出来的血泪经验。4.1 STM32芯片加载hex后完全没反应这是第一个翻车点。现象是点击运行后芯片引脚上没有任何波形LED不亮电机不动仿佛程序根本没烧进去。原因通常有两个。第一MDK工程没有勾选生成hex文件默认只输出axfProteus里选了axf文件虽然能加载但运行行为不稳定。第二hex文件放在带中文或空格过多的路径下Proteus解析固件失败。解决方法是回到MDK的Options for Target界面在Output页勾选Create HEX File重新编译后在工程目录的Objects文件夹下找到hex文件。在Proteus里双击STM32F103C8芯片在Program File一栏选择该hex确保路径全英文。改完后再运行用示波器点一下PA0立刻能看到方波。4.2 电机不转但PWM波形正常现象是示波器上PWM波形漂亮完整L298N输入引脚也有信号但两个MOTOR-DC纹丝不动。这时最容易怀疑仿真模型坏了其实是接线和使能问题。原因是L298N的ENA、ENB使能脚悬空了。这个芯片的使能端不能靠内部上拉自动开启悬空时输出级全关断PWM进多少都白搭。另一个原因是电机供电VS没有真正接12V或者VS和VSS没有共地。解决方法是把ENA和ENB分别用导线连接到3.3V或5V确认VS接12V、VSS接5V并且两个电源的GND连到同一网络。如果用了PWM调速ENA就不要接固定高电平而是接PA0。这一点我在2.2节接线步骤里特别强调过真出了问题优先查共地。4.3 电机转速不受控占空比改大反而停转现象是让占空比从30改成80转速反而骤降甚至停转。从波形上看出占空比确实变了但执行器响应完全反向。原因是占空比数值超过定时器自动重装载值arr。arr99时CCR寄存器如果写入100PWM输出Pin维持持续高电平占空比为0%。这是一个隐蔽的逻辑错误寄存器不会报告异常。解决方式是在Motor_Control入口做饱和处理超过99就钳位到99。另一处容易忽略的是TIM_SetCompare1传入的参数类型是uint32_t如果上层变量是无符号char且被算法计算成0~255范围需要映射到0~99。我给这个函数加了一个简单的线性缩放duty duty * 99 / 255让调速接口更通用。4.4 循迹小车左右摆动像是在画蛇现象是小车能走但车头不断左右摇摆轨迹明显不是直线而是幅度很大的蛇形线。原因是差速值过大。循迹决策里转向时一侧70一侧35差速35%对急弯有效但对直线微偏太敏感。传感器一碰到线边缘就猛打方向越过中线又反向猛打形成持续振荡。解决方法是把差速拆成两档。偏离幅度小的时候用缓修正比如60和45偏离幅度大的时候才用急修正。双传感器方案其实无法判断偏离大小所以更实用的办法是降低基础转速把直行速度从60降到50同时把转向差速改成25%。仿真里调到这个组合后蛇形现象基本消失。4.5 仿真运行几秒后程序复位LED闪烁现象是仿真运行得挺流畅但每隔几秒整个系统就像断电重启一样GPIO全部回到初始状态。原因是看门狗没有关闭或喂狗不及时。STM32的IWDG在标准外设库里默认是关闭的但有些从网上复制的初始化代码会打开看门狗却没有在主循环喂狗。仿真模式下指令执行速度和真实芯片不同喂狗间隔容易超时触发复位这才是黑匣子一样的坑。解决方法是检查SystemInit代码里有没有IWDG相关寄存器操作如果有确认主循环每圈调用IWDG_ReloadCounter()。更稳妥的做法是注释掉看门狗初始化代码仿真阶段根本不需要它。等移植到真实板卡前再按实际情况打开这是给初学者最大的后悔药。5. 验收与答辩从功能测试到演示评分的完整闭环方案做完了最后一步是验收。我说过很多次课程设计和项目答辩的分数差距往往不在功能多少而在验收过程是否可信。一个能跑但说不清楚为什么这样跑的小车远不如一个功能列表清晰、测试记录完整的小车得分高。5.1 验收用例与记录表建议在答辩前把下面三件事做成一张表格逐项打勾。第一项测试PWM信号用Proteus虚拟示波器探针接PA0确认输出频率约10kHz且占空比能随TIM_SetCompare1参数变化。第二项测试电机换向把左电机方向改反观察MOTOR-DC是否反转。第三项测试循迹决策依次按下左传感器按键、右传感器按键、双按键观察电机转速变化是否符合3.3节的决策逻辑。用例操作预期结果PWM波形示波器探针接PA010kHz方波占空比可调电机转向切换left_dir参数电机反转循迹修正按下左传感器左轮加速右轮减速丢线停车释放全部按键电机停止5.2 演示前强制走一遍的四步检查每次演示前我都有固定的检查顺序。第一步看hex路径确认编译时间是最新的避免演示时加载旧固件。第二步看电源网络用Proteus的电气规则检查工具扫一遍确认没有悬空电源引脚本。第三步看传感器按键逐个按下再释放确认LED指示对应变化。第四步看示波器预先切换到PA0通道让评审看到稳定的PWM波形。这四步走完演示翻车的概率已经降到很低。答辩时最常见的追问是「为什么选10kHz」「为什么用L298N不用其他驱动」。回答L298N那题时可以从电流能力切入它每个通道能驱动2A左右电流直-flow卡跑得动、余量也够比一堆分立三极管搭的H桥可靠得多。10kHz的来历用72MHz除以预分频和重装载值现场算给评审看这比背概念更有说服力。我说一件自己身上的事有一次做的某小车Demo在仿真里好好的一接实物就疯狂复位查了两天发现是外部晶振型号和代码里HSI配置对不上。从那以后我每次拿到新仿真工程第一件事不是跑代码而是先把时钟配置、引脚复用表、电源共地这三项列成清单过一遍再开始搭电路。希望帮到你。本文还有配套的精品资源点击获取