简介:基于STM32与Proteus的智能小车仿真项目,以STM32F103C6为主控,面向嵌入式电机控制初学者,提供无需实物即可验证小车运动逻辑的完整工程方案。资源共155个文件,压缩包6.08MB,主要文件涵盖C语言源码与头文件、编译中间文件、Keil开发环境工程配置、Proteus仿真工程以及启动与链接脚本,适合对照源码、电路和仿真状态学习。小车利用按键分别控制双电机正反转,并借助脉宽调制调节转速,覆盖通用输入输出、定时器、中断等STM32核心外设应用;在虚拟环境中先行调试验证,可显著减少硬件试错成本。包内工程备份与工作区文件还能辅助理解从底层初始化到运动控制逻辑的完整流程。通过仿真可在无硬件条件下反复验证代码正确性,减少实物实验中的错误与时间成本,也为后续更复杂的机器人项目打下基础。已有10226人学习下载,适合作为STM32入门、电机驱动及Proteus仿真开发的实战参考。
1. 用Proteus跑STM32智能小车:先仿真后动手,到底省了什么
“智能小车”这个题目在嵌入式课设里快被写烂了,但很多人第一版实物翻车的细节,其实都能在仿真里提前测出来:电机一侧转一侧不转,先查共地;循迹阈值不对,先查传感器供电;PWM频率不对,电机吼叫但就是不动。如果你能在Proteus里先把STM32F103C8的引脚映射、PWM参数、循迹和避障逻辑跑通,再去画板子、焊板子,至少能省下一个通宵的飞线排查时间。这篇笔记就是讲这套仿真怎么做:从Keil工程到Proteus接线,从驱动层到感知层,再到5个最典型的翻车点。适合正在做课设或毕业设计的同学,也适合想先在仿真里验证完逻辑再动手的开发者——仿真不是玩具,它是给实物调试铺路的。
2. Proteus + Keil 工程搭建:让 STM32F103C8 在仿真图里先跑起来
2.1 版本选型与器件库:先确认你的 Proteus 能搜到 STM32F103C8
Proteus 对 STM32 的支持是最近几个大版本才完善的,如果你打开元件库搜不到 STM32F103C8,多半是库版本太老或者没有安装对应的芯片库。常见的做法是直接用 8.x 之后的版本,元件库搜索框里输入STM32F103C8,然后从分类Microprocessor ICs -> ST ARM Cortex里拖出来。注意,有些下载的精简版 Proteus 会把 ARM 库裁掉,装完以后先做一个测试:在库管理器里搜LM324这种通用运放,如果能搜到但搜不到 STM32,那就是芯片库缺失,需要补装。
Keil 这边建议直接用 MDK 5 的社区版或标准版,新建工程时选择芯片型号为 STM32F103C8。这里有个细节:Keil 工程里的芯片型号不会影响最终生成的 HEX 文件内容,但会影响编译器对寄存器地址的判断,所以型号选错可能导致链接器报错。工程创建完成后,在 Options for Target 里做三件事:第一,Output 选项卡勾选 Create HEX File;第二,Debug 选项卡选择 Use Simulator,别选 ULINK 或 ST-Link;第三,C/C++ 选项卡里把优化等级设为 -O0 或 -O1,方便调试。
Proteus 里加载固件的路径是双击仿真图里的 STM32 芯片,在 Program File 一栏选择 Keil 输出目录下的 HEX 文件。加载完成后点左下角的运行按钮,如果芯片有点反应,说明整体链路已经通了。这里我建议第一步别直接写小车逻辑,先做一个闪灯程序确认最小系统没问题,否则后面遇到问题你分不清是硬件仿真图的问题还是代码的问题。
2.2 最小系统接线与时钟设置:仿真图里的晶振是个黑匣子
很多第一次用 Proteus 仿真的同学,会照着实物最小系统的画法给 STM32 接 8M 晶振、两个 20pF 电容、复位电路,结果发现仿真跑不起来或者芯片完全不工作。这里有个关键的认知差别:Proteus 的 STM32 模型在大多数版本里并不真正依赖外接晶振起振,芯片的工作主频是在属性对话框里设置的,外接晶振只影响 HSE 引脚的逻辑电平,不参与内部 PLL 的计算。
所以我的习惯是:仿真图里不画外部晶振电路,直接双击芯片,在 Component Properties 里找到 Clock Frequency 这一项,设成 72MHz。然后代码里保持 Keil 默认的 SystemInit 逻辑不动,这样 SystemCoreClock 就是 72MHz。如果你在代码里手动配置了 HSE 或 PLL 倍频系数,反而容易和仿真模型的时钟对不上,出现定时器时间全部偏慢或偏快的诡异现象。
电源和地线倒是不能马虎。VDD 接 3.3V,VSSA 和 VDDA 也接 3.3V,GND 统一接地。仿真模型对复位脚 NRST 一般不太敏感,但建议接一个 10k 上拉到 3.3V,防止某些版本的模型因为复位脚悬空而出现随机复位。BOOT0 和 BOOT1 直接接地,不要悬空。这些引脚在仿真模型里不接也能跑,但养成和实物一致的习惯,后面移植代码时少改一个是一个。
2.3 Keil 工程的三个关键配置:输出 HEX、仿真模式与 Flash 下载设置
工程建好后,最容易翻车的三个配置点如下。第一,Output 选项卡的 Create HEX File 必须勾选,否则你编译一万次也生成不了 HEX 文件,Proteus 那边自然无程序可加载。第二,Debug 选项卡要选 Use Simulator,并且在旁边设置 Dialog DLL 参数时留空默认值,不要填成 ST-Link 的 DLL,否则编译和调试会进入一个找硬件的死循环。第三,Utilities 选项卡里的 Flash Download 设置对仿真无用,但建议把 Reset and Run 勾上,这样以后接实物调试器时不用再改配置。
代码文件组织方面,我建议三个文件起步:main.c只放主循环和状态路由,driver.c放电机、PWM、串口这类硬件初始化,sensor.c放循迹和超声波的数据读取与处理。不要把所有初始化堆在 main 函数里,因为仿真调试时你想注释掉某一段逻辑,文件之间耦合太紧会牵一发动全身。
仿真工程还有一个好处:修改代码后重新编译生成 HEX,在 Proteus 里点暂停再点运行就能热加载,不需要重启仿真。但注意,如果芯片正在运行状态直接加载新 HEX,有些版本会出现程序区更新不同步的问题,保险起见先暂停仿真再加载。这一步的血泪经验是:每次修改代码后,先确认 Keil 编译输出框里出现了 "Build Target Completed" 且没有 Error,再去加载 HEX,不要编译报错还硬加载旧文件,那样你排查半天还以为代码没写对。
3. 驱动层设计:L298N 接线、PWM 调速与差速转向参数
3.1 L298N 在仿真里的接法与 IO 映射
电机驱动方案里,L298N 是课设和仿真中最常见的模块,不是因为性能最好,而是它的逻辑电平容忍度高、接法直观。Proteus 元件库搜索 L298N 或 L298,能找到一个独立的电机驱动芯片模型。这个模型有 IN1~IN4 四个逻辑输入、ENA 和 ENB 两个使能端、OUT1~OUT4 四个输出端,以及 VSS 和 VS 两个电源引脚。
接法上我的习惯是:VSS 接 5V(逻辑电源),VS 接 12V(电机电源),GND 和 STM32 的 GND 必须共地。输出 OUT1、OUT2 接左侧电机两端,OUT3、OUT4 接右侧电机两端。IN1~IN4 接到 STM32 的 GPIO,ENA 和 ENB 接 PWM 输出通道,用来调速。下面给一个我常用的引脚映射,直接在代码里用宏定义写死,后面移植到实物时只改这个文件即可。
// motor_io.h #define MOTOR_LEFT_IN1 GPIO_Pin_0 // GPIOB.0 -> L298N IN1 #define MOTOR_LEFT_IN2 GPIO_Pin_1 // GPIOB.1 -> L298N IN2 #define MOTOR_RIGHT_IN3 GPIO_Pin_2 // GPIOB.2 -> L298N IN3 #define MOTOR_RIGHT_IN4 GPIO_Pin_3 // GPIOB.3 -> L298N IN4 #define MOTOR_LEFT_PWM TIM2_CH1 // PA0 -> L298N ENA #define MOTOR_RIGHT_PWM TIM2_CH2 // PA1 -> L298N ENB这里有一个很多人踩过的坑:IN1 和 IN2 同时为高电平或同时为低电平时,电机处于刹车或自由停止状态;只有一高一低时电机才转动。很多同学初始化时把 IN1~IN4 全部设成低电平,然后只调 PWM,发现电机完全不动,还以为是 PWM 通道配置错了,其实根本没有形成差模电压。L298N 的使能端 ENA/ENB 如果直接接 5V 不做 PWM,电机就是以全速转,只有把使能端接到 PWM 输出上,才能实现调速。仿真里一样有这个行为,接错了现象和实物完全一致。
3.2 PWM 频率与占空比:这三个参数决定电机听不听话
STM32 定时器输出 PWM 时,核心参数就三个:预分频器 PSC、自动重装载值 ARR、捕获比较值 CCR。输出频率的计算公式是:PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1),占空比是CCR / (ARR + 1)。以 STM32F103C8 的 APB1 定时器时钟 72MHz 为例,想要 1kHz 的 PWM,可以设 PSC = 71,ARR = 999,那么频率就是 72MHz / 72 / 1000 = 1kHz。占空比 50% 对应的 CCR 就是 500。
// driver.c - TIM2 输出 PWM 通道1和通道2 void TIM2_PWM_Init(uint16_t freq_hz, uint16_t duty_percent) { GPIO_InitTypeDef gpio_init; TIM_TimeBaseInitTypeDef tim_base; TIM_OCInitTypeDef tim_oc; uint16_t arr, psc, ccr; // 计算 ARR 和 PSC,保证频率落在合理区间 arr = 1000 - 1; // 自动重装载值:决定 PWM 周期 psc = (72000000UL / (arr + 1)) / freq_hz - 1; // 预分频:72MHz / 分频后 = 目标频率 ccr = (arr + 1) * duty_percent / 100; // 占空比:CCR / (ARR+1) // 时钟使能:TIM2 挂在 APB1,GPIOA 挂在 APB2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置 PA0、PA1 为复用推挽输出,50MHz 速度 gpio_init.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; gpio_init.GPIO_Mode = GPIO_Mode_AF_PP; gpio_init.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio_init); // 定时器时基:向上计数,PSC 和 ARR 生效 tim_base.TIM_Prescaler = psc; tim_base.TIM_CounterMode = TIM_CounterMode_Up; tim_base.TIM_Period = arr; tim_base.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, &tim_base); // PWM1 模式:CNT < CCR 时输出高电平 tim_oc.TIM_OCMode = TIM_OCMode_PWM1; tim_oc.TIM_OutputState = TIM_OutputState_Enable; tim_oc.TIM_Pulse = ccr; tim_oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &tim_oc); TIM_OC2Init(TIM2, &tim_oc); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_ARRPreloadConfig(TIM2, TIM_ARRPreload_Enable); TIM_Cmd(TIM2, ENABLE); }占空比设置方面,我建议初始给 50% 而不是 100%。因为仿真里电机模型对电压突变很敏感,100% 占空比下从静止到全速的冲击会让小车起步时明显抬头,而且转向时冲击电流会让虚拟示波器上的波形出现毛刺。先把占空比设在 40%~60% 之间,确认电机转动方向和转速变化符合预期,再逐步调高。有一点需要特别说明:仿真里 1kHz 的 PWM 能看到明显的轮速脉动,这是模型对低频 PWM 的自然响应,不影响逻辑验证;而实物直流电机一般推荐 10kHz~20kHz 以避免啸叫。所以仿真调逻辑、实物调频率,这个顺序不要反过来。
3.3 左右轮差速转向:加减速的幅度才是转向质量的关键
差速转向的思想很简单:左转时左轮减速、右轮加速,小车就能向左偏转。但具体减多少、加多少,直接决定小车是平稳过弯还是原地打转。一个容易犯的错误是把转向差速设得过大——左轮 10%、右轮 90%,结果小车不是转向,而是绕着左轮中心点旋转,循迹时轨道完全丢失。
我常用的策略是:直行时左右轮都是 50% 占空比;轻度转向时一侧 40%、另一侧 60%;急转时一侧 20%、另一侧 80%。这个比例要配合转向持续时间来调,仿真里每次转向控制周期建议 20ms,不要让转向行为一次性持续几百毫秒,否则必然过冲。
// driver.c - 电机速度控制接口 #define PWM_STRAIGHT 50 // 直行占空比 #define PWM_TURN_LOW 30 // 转向内侧轮 #define PWM_TURN_HIGH 70 // 转向外侧轮 void Set_Motor_Left(uint16_t duty) { TIM_SetCompare1(TIM2, (1000 * duty) / 100); // CCR1 = 占空比 * ARR } void Set_Motor_Right(uint16_t duty) { TIM_SetCompare2(TIM2, (1000 * duty) / 100); // CCR2 = 占空比 * ARR } void Go_Straight(void) { Set_Motor_Left(PWM_STRAIGHT); Set_Motor_Right(PWM_STRAIGHT); } void Turn_Left(void) { Set_Motor_Left(PWM_TURN_LOW); Set_Motor_Right(PWM_TURN_HIGH); } void Turn_Right(void) { Set_Motor_Left(PWM_TURN_HIGH); Set_Motor_Right(PWM_TURN_LOW); }这里的TIM_SetCompare1和TIM_SetCompare2是标准库的寄存器写接口,直接修改 CCR 值,下次计数器溢出时自动生效。注意修改的时机:不要在计数器计数到一半时频繁修改 CCR,否则 PWM 波形会出现一次畸变,仿真里不明显,实物上表现为电机抖动。建议所有转向控制放在同一个函数里顺序执行,保证左右两个通道的 CCR 在同一时间点更新。还有一个容易被忽略的细节:差速转向时内侧轮不要减到 0,一旦 PWM 占空比降到 0,电机相当于断电自由转动,轮胎会失去抓地力,在循迹场景里表现为车身横摆。保持 20% 以上的最低占空比能避免这个问题。
4. 感知与决策:循迹阈值标定与超声波避障的协同逻辑
4.1 红外循迹模块的仿真建模:用光敏电阻近似红外反射检测
Proteus 元件库里没有直接叫“红外循迹传感器”的标准型号,常见的替代方案有两种:一种是直接用 GPIO 引脚的高低电平来模拟传感器输出,调试时手动切换,代码和实物一致;另一种是用光敏电阻加比较器搭一个光电检测电路,电路行为更接近真实情况,但调试复杂度高不少。
我一般先用手动电平模拟的方式跑通逻辑,再决定要不要换成光敏电阻方案验证阈值。因为循迹算法的核心是:传感器返回的到底是黑线状态还是白底状态,你的代码对这个状态的判断是否稳定。如果逻辑本身就不对,传感器建模再真实也只是在错误的基础上修修补补。
// sensor.h - 循迹传感器引脚定义 #define TRACK_LEFT_GPIO GPIOB #define TRACK_LEFT_PIN GPIO_Pin_4 // 左传感器 -> PB4 #define TRACK_RIGHT_GPIO GPIOB #define TRACK_RIGHT_PIN GPIO_Pin_5 // 右传感器 -> PB5 // 返回 1 表示检测到黑线,0 表示白底 uint8_t Read_Track_Left(void) { return GPIO_ReadInputDataBit(TRACK_LEFT_GPIO, TRACK_LEFT_PIN); } uint8_t Read_Track_Right(void) { return GPIO_ReadInputDataBit(TRACK_RIGHT_GPIO, TRACK_RIGHT_PIN); }注意 GPIO 初始化的模式要设成GPIO_Mode_IPU(上拉输入)或者GPIO_Mode_IN_FLOATING(浮空输入)。传感器模块输出端如果是开漏结构,必须用上拉输入,否则读到的电平不稳定;如果是推挽输出,浮空也没问题。仿真里手动模拟高低电平时,直接用电源或地接到引脚即可,但在引脚上串一个 10k 电阻再接入,更贴近实物传感器模块的输出阻抗。
4.2 循迹阈值与死区:双路传感器的状态判断别写得太脆
双路循迹是课设里最常见的形式,两个传感器并排安装在车头底部,车走歪时一边压到黑线、另一边在白底上,控制器根据两路信号修正方向。最容易翻车的写法是直接比较两个引脚电平然后立即转向,没有去抖、没有死区、没有周期控制,结果小车在阈值边缘疯狂震荡,表现为走 S 形甚至原地摆动。
正确的做法是加一个状态合并函数,把两路传感器的读数映射成四个状态:左中右和直行。同时给传感器读数一个持续性的判断窗口——连续读到同一个状态 5 次才认为状态有效,这能滤掉仿真步长带来的瞬间抖动。
// sensor.c - 循迹状态合并与去抖 #define TRACK_STATE_STRAIGHT 0 #define TRACK_STATE_LEFT 1 #define TRACK_STATE_RIGHT 2 #define TRACK_STATE_BOTH 3 uint8_t track_state = TRACK_STATE_STRAIGHT; uint8_t track_debounce_count = 0; void Track_Update(void) { uint8_t left = Read_Track_Left(); uint8_t right = Read_Track_Right(); uint8_t new_state; // 状态映射:左黑右白 -> 左偏,右黑左白 -> 右偏 if (left == 1 && right == 0) { new_state = TRACK_STATE_LEFT; } else if (left == 0 && right == 1) { new_state = TRACK_STATE_RIGHT; } else if (left == 1 && right == 1) { new_state = TRACK_STATE_BOTH; // 交叉线或出线 } else { new_state = TRACK_STATE_STRAIGHT; } // 连续 5 次一致才切换状态,滤除抖动 if (new_state == track_state) { track_debounce_count = 0; } else { track_debounce_count++; if (track_debounce_count >= 5) { track_state = new_state; track_debounce_count = 0; } } } void Track_Control(void) { Track_Update(); switch (track_state) { case TRACK_STATE_LEFT: Turn_Left(); // 车头偏左,向右修正 break; case TRACK_STATE_RIGHT: Turn_Right(); break; case TRACK_STATE_BOTH: Go_Straight(); // 交叉线,保持直行 break; default: Go_Straight(); break; } }这里有个容易混淆的地方:左传感器检测到黑线,说明车头已经偏向线左侧,此时应该让小车向右修正,而不是继续向左。很多同学在这里写反,导致小车越偏越多。去抖计数的数值 5 对应的时间取决于主循环周期,如果主循环是 10ms 跑一次,那么 5 次就是 50ms,这个响应速度在小车速度不高时是合适的,如果车子跑得快,可以缩短到 3 次。
循迹状态TRACK_STATE_BOTH表示两个传感器同时压线,这在正常的直线赛道里几乎不会出现,但十字路口或障碍物区域会出现。我建议在这个状态下保持直行,而不是原地停止,因为很多场地赛的十字交叉口就靠这个逻辑直接冲过去。
4.3 超声波避障:Echo 高电平测距的实现与仿真注入
超声波避障的原理是:Trig 脚拉高 10us 以上触发测距,模块发出声波后 Echo 脚输出高电平,高电平持续时间就是声波往返时间。距离 = 时间 × 声速 / 2,常温下声速取 340m/s。STM32 测高电平时长最常见的做法是用定时器输入捕获,或者简单一点用定时器计时加口循环读取。
// sensor.c - 超声波测距(毫秒级超时保护) float Ultrasonic_Read_Distance(void) { uint32_t counter = 0; float distance = 0.0f; GPIO_SetBits(GPIOB, GPIO_Pin_6); // TRIG 拉高 Delay_us(15); // 至少 10us,留点余量 GPIO_ResetBits(GPIOB, GPIO_Pin_6); // TRIG 拉低 // 等待 ECHO 变高,带 10ms 超时保护 while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == 0) { counter++; if (counter > 10000) return -1.0f; // 超时返回 -1 Delay_us(1); } // 测量 ECHO 高电平持续时间,计数器单位 us counter = 0; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) == 1) { counter++; if (counter > 30000) return -1.0f; // 超过 30ms,约 5m 距离 Delay_us(1); } distance = (float)counter * 340.0f / 2.0f / 1000.0f; // 单位 cm return distance; }仿真里没有真实的超声波模块时,我建议自己写一个测试函数代替Ultrasonic_Read_Distance(),直接返回固定距离来验证避障状态机,因为状态机的逻辑判断才是要验证的目标,Echo 引脚的真实电平时序在实物调试时自然会验证到。
// debug.c - 固定距离注入测试 float Test_Distance_Inject(void) { static uint8_t step = 0; // 模拟一个从 50cm 到 10cm 再到 50cm 的障碍物过程 step = (step + 1) % 40; if (step < 20) return 50.0f - step; else return step - 30.0f + 10.0f; }避障逻辑一般是三态状态机:搜索前进、左转、右转。我的实现是车头在前方障碍物距离小于 25cm 时停车,向左转 500ms,如果左转后前方距离仍小于 25cm 就向右转,否则继续前进。转向持续时间这个参数最容易翻车:转太久会把车头转向 180 度,转太短又避不开障碍物。仿真里先用固定距离注入测试状态切换逻辑,确认每个状态都能正确跳出,然后再去调转向时间。
5. 仿真调试避坑:5 个从黑屏到飞车的高频问题
5.1 现象:加载 HEX 后点运行,芯片完全不响应
仿真图点运行后,STM32 芯片既没有 IO 变化,也没有电流指示,就像死机一样。最常见的原因是 HEX 文件加载路径有问题,或者芯片属性里的 Clock Frequency 和代码里的系统时钟不一致。Keil 生成的默认 SystemInit 会把时钟倍频到 72MHz,而 Proteus 芯片属性默认的 Clock Frequency 往往不是 72MHz,两边对不上,定时器外设的时钟源就乱了。
解决方法是双击仿真图里的 STM32 芯片,把 Clock Frequency 属性显式改成 72MHz。同时确认 HEX 文件路径不要包含中文字符,Proteus 对路径里的非 ASCII 字符支持有问题,这个看起来不起眼却非常致命。改完以后先暂停再重新运行,如果还不响应,在代码入口放一个 GPIO 翻转的最小程序,排除加载问题。
5.2 现象:程序在跑、LED 亮,但电机不转
这个现象出现时,通常 GPIO 逻辑输出是对的,但电机驱动链路断了。先查 L298N 的 VSS 和 GND 是否接对,VSS 必须和 STM32 共地,否则逻辑输入信号根本没有参考电平。再查 ENA/ENB 是否接在 PWM 输出通道上,很多同学把使能端接到了普通 GPIO,然后只控制 IN1~IN4,电机当然不会调速。仿真里有一个隐蔽的坑:L298N 的 VS 端如果没有接 12V 电机电源,OUT 端不会有任何输出,但芯片表面看不出异常。
解决步骤是:先用万用表探针(Proteus 的 Voltage Probe)量 OUT1 和 OUT2 之间的电压,如果始终为 0,往前查 VS;如果有电压但不转,查电机模型的额定电压,Proteus 默认电机模型可能需要更高的驱动电压才能转动。把电源电压设到 12V,再把 ENA/ENB 直接接 5V 排除 PWM 配置问题,确认电机能转了,再接回定时器通道。
5.3 现象:循迹小车走 S 形,左冲右撞
走 S 形的直接原因是转向修正过猛或者控制周期太短。很多同学写的循迹循环是 1ms 一次,每次转向都执行满幅差速,结果小车像蛇一样扭来扭去。另一个隐蔽原因是传感器去抖没做好,在临界状态引脚电平抖动,状态机频繁切换。
我的解决习惯是:把控制周期拉长到 10ms 一次,转向幅度从 ±30% 起步而不是直接 ±80%。去抖窗口设 5 次连续采样。如果还走 S 形,把直行速度占空比从 50% 降到 30%,让小车速度慢下来,这样同样幅度的差速产生的角速度更大、更可控。
5.4 现象:仿真速度慢到没法调
Proteus 仿真跑起来像幻灯片,尤其是加了超声波测距里那个 10us 级别的 Delay 循环后,传感器越多、动画更新率越高,仿真越慢。原因在于仿真模型是逐条指令模拟的,大量的Delay_us空循环会消耗巨量的仿真计算资源。
解决方法是把动画帧率降低:菜单 Debug -> Simulator Options,把 Animation Update Rate 调低,或者选择 Run at Real Time Confirmed 之外的 Maximum 模式。同时把主循环里的延时改成定时器分时调度,不要用阻塞式 Delay。超声波测距里的轮询循环也可以改成条件计数,用定时器中断记录时间戳,避免模拟器去执行百万次空循环。仿真速度一快,整个调试循环的体验完全不同。
5.5 现象:ADC 采集的值乱跳,循迹误判
如果你用 ADC 模式读循迹传感器而不是数字量,仿真里的读数会显得比实物噪声大得多,原因是 Proteus 的 ADC 模型受仿真步长和电源纹波影响明显,采到的数值在十几到几十之间随机抖动。直接拿这个原始值做阈值判断,误判率会非常高。
解决方法是软件滤波:连续采样 5 次取中值或者平均,再和阈值比较。更稳一点的做法是在比较器端加迟滞:阈值设成两个值,从高变低和从低变高之间隔一个不敏感区间。比如识别黑线的阈值设 2.0V,识别白底的阈值设 1.5V,中间 0.5V 的区间不做判断,这样能有效避免临界抖动。
6. 验证手段与进阶:虚拟示波器看 PWM、串口打印状态机信息
前面把逻辑跑通了,但“跑通”和“跑对”是两码事。我的习惯是每次调参都用 Proteus 自带的虚拟示波器看一下 PWM 波形,而不是只看小车动不动。把虚拟示波器的通道 A 接在 PA0 上,通道 B 接在 PA1 上,就能同时看到左右两路 PWM 的波形和各自的占空比。
常见的做法是在 Proteus 左侧工具栏选择 Virtual Instruments,拖出 OSCILLOSCOPE,然后接两根探针线到 PWM 输出引脚。如果波形显示高电平时间明显大于低电平时间,说明占空比偏大;如果频率和设置值差很多,回查 PSC 和 ARR 的配置是否和时钟频率匹配。这个检查 30 秒就能完成,比实物上拿电表量快得多。
另一个很好用的验证工具是 Virtual Terminal。把 USART1 的 TX 引脚接到虚拟串口终端上,代码里初始化串口后用 printf 输出当前状态机、传感器读数和 PWM 占空比。注意虚拟终端的波特率要和代码设置一致,默认 115200、8 位数据、1 位停止位,如果出现乱码,先检查配置再检查引脚接线。
// debug.c - printf 重定向到 USART1 int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); return ch; }把调试信息打印出来以后,把状态机的所有状态转换打上时间戳,比对着虚拟示波器的波形看,基本能定位大多数逻辑问题。我第一次做智能车仿真时,因为一个 PWM 频率设置不对,花了整整一个晚上在实物上调电机,后来回头用虚拟示波器看波形才发现占空比根本不对,那一步如果早点做,至少省三个小时。从那以后我养成了个习惯:仿真里验证过的参数必须在笔记里记录,频率、占空比、去抖次数、转向时间,全写清楚。因为实物调试时你会发现,所有莫名其妙的问题,最后都能在仿真参数里找到先兆。希望帮到你。
本文还有配套的精品资源,点击获取