简介:本资源是一套完整的基于STM32单片机的出租车计价器毕业设计工程资料,面向电子信息、自动化及嵌入式方向的本科生与初学者,解决课程设计、毕设开发中软硬件协同实现计费逻辑、传感器信号采集、LED/LCD显示及实时时钟管理等典型问题。压缩包含266个文件,总大小23.88MB,涵盖36个C源文件(含stm32f10x系列外设驱动与计价核心算法)、38个H头文件(定义硬件接口与功能模块)、2个原理图(.SchDoc)与2个PCB图(.PcbDoc)、7份PDF参考文档(含设计报告与器件手册)、30个编译中间文件(.o/.d)及Keil工程配置文件(.uvproj/.uvopt),并附有元件清单Excel与器件资料。已有4181人学习下载,资料结构完整、工程可直接编译运行,特别适合通过真实项目掌握ARM Cortex-M架构开发流程、STM32底层驱动编写、硬件电路识图与PCB布局要点,是嵌入式硬件实践的高复用性范例。
1. 项目概述与核心需求拆解
“基于STM32的出租车计价器系统”,这个毕设题目乍一看挺传统,但真要把它做扎实、做出彩,里面门道可不少。它绝不仅仅是把里程和价格乘起来显示那么简单。我当年带学生做类似项目,发现很多新手容易陷入“功能堆砌”的误区,而忽略了系统作为一个完整嵌入式产品所必需的可靠性、实时性和可维护性。这个项目的核心,是模拟一个真实、可用的计价终端,它需要稳定地处理来自传感器的脉冲信号,在复杂的现实场景(如低速蠕动、停车等待、夜间加价)中准确计算费用,并通过友好的人机界面进行交互和存储。
这个系统适合谁呢?首先是电子信息、自动化、物联网等相关专业的本科生,作为毕业设计或课程设计,它能全面考察你对单片机原理、传感器应用、实时操作系统(或裸机调度)和人机交互的理解。其次,对于刚接触STM32的嵌入式爱好者,这也是一个绝佳的练手项目,涵盖了GPIO、定时器、中断、ADC、OLED/LCD显示、按键输入、数据存储等大部分常用外设。通过它,你能把书本上的知识点串成一条线,真正理解一个产品从需求分析到代码实现的完整流程。
接下来,我会抛开那些华而不实的理论,直接切入实战,从设计思路、硬件选型、软件架构,到代码实现和调试避坑,把我在多个类似项目中积累的经验和教训一次性讲透。目标很简单:让你看完就能动手,做出来就能稳定运行。
2. 系统整体设计与硬件选型思路
做一个计价器,我们得先想清楚它要干什么。核心功能无非几点:测距、计时、计费、显示、存储和按键控制。但怎么实现这些功能,选什么硬件,直接决定了项目的难度和成败。
2.1 核心控制器:为什么是STM32F103C8T6?
市面上单片机那么多,51、AVR、STM8,为什么这个项目通常首选STM32,尤其是F103系列(如C8T6)?原因很实际:
- 性能与资源充足:72MHz主频,20KB RAM,64KB Flash。处理计价逻辑、驱动显示、管理外设绰绰有余,避免了51单片机可能遇到的计算瓶颈或内存紧张。
- 丰富的外设:这是关键。我们需要定时器的编码器模式来精准测量车轮脉冲;需要高级定时器的PWM输出驱动蜂鸣器;需要多个USART,一个可能接GPS模块(用于高级功能),一个用于调试打印;需要SPI/I2C驱动OLED屏和存储芯片;需要多个ADC通道监测电池电压(如果设计为便携式)。STM32F103恰好提供了这套“全家桶”。
- 开发生态成熟:Hal库、标准库资料海量,调试工具(ST-Link)便宜易用,社区支持强大。这能极大降低开发门槛,让你把精力集中在业务逻辑,而不是折腾底层驱动。
- 成本与封装:C8T6的TSSOP20封装,体积小,引脚数适中,既能满足需求,又方便焊接和布线,性价比极高。
注意:如果项目要求极低成本,且功能极其简单(只显示金额),可以考虑STM32F030系列。但如果涉及稍复杂的菜单、数据存储或未来扩展,F103仍是更稳妥的起点。
2.2 传感器选型:里程测量的心脏
里程测量是计价的基础,通常有两种方案:
- 方案A:霍尔传感器+磁铁。在车轮或传动轴上安装磁铁,旁边固定霍尔传感器。车轮每转一圈,磁铁经过传感器,产生一个脉冲。这是最经典、最可靠、成本最低的方案(几块钱)。我们需要计算轮胎周长,然后通过脉冲数换算里程。例如,轮胎周长0.5米,每圈产生4个脉冲(用四极磁铁或倍频电路),那么每个脉冲代表0.125米。
- 方案B:光电编码器。精度更高,能直接输出A、B两相正交脉冲,通过STM32定时器的编码器接口可以直接读取方向和计数,抗干扰能力更强,但成本也更高(几十元)。
对于出租车计价器毕设,方案A(霍尔传感器)完全足够且更贴近实际。我们需要关注的是传感器的信号调理。霍尔传感器输出可能是缓慢变化的模拟信号或开关信号,需要经过斯密特触发器(如74HC14)或简单RC滤波整形后,再接入单片机的GPIO,以消除抖动和毛刺。
2.3 人机交互模块:显示与输入
- 显示单元:0.96寸OLED(SSD1306驱动,I2C接口)是首选。理由:无需背光、对比度高、功耗低、接口简单(仅需2根线)、显示效果细腻。128x64的分辨率足以显示金额、里程、时间、状态等信息。LCD1602虽然更便宜,但显示内容单调,视觉效果远不如OLED。
- 输入单元:通常需要4-6个独立按键。建议布局为:“模式切换”(空车/载客)、“开始/停止”、“单价查询/设置”、“数据清除”、“翻页”等。按键电路务必加上硬件消抖(如104电容并联)和上拉电阻,软件中再配合延时消抖,能省去很多误触发的调试麻烦。
- 声光提示:一个蜂鸣器(用三极管驱动)用于开始计费、结束计费、按键提示。一个LED指示空车/载客状态。
2.4 数据存储单元:为什么需要EEPROM?
计价器掉电后,单价、总营收、当日营运次数等数据不能丢失。STM32的内部Flash虽然可以读写,但寿命有限(约1万次),且操作复杂易出错。因此,外挂一颗I2C接口的EEPROM(如AT24C02/04/16)是标准做法。它专为频繁的小数据量存储设计,寿命可达百万次。我们将系统参数(白天/夜间单价、起步价、起步里程、等待时间单价等)和累计营运数据存储于此。
2.5 电源管理设计
这是一个容易被忽视但至关重要的部分。系统可能由车载12V电源或电池供电。我们需要一个DC-DC降压模块(如LM2596)将输入电压稳定到5V,再用LDO(如AMS1117-3.3)得到稳定的3.3V给STM32和大部分外设供电。务必在电源入口处加入反接保护二极管和滤波电容,以提高系统的稳定性和抗干扰能力。
3. 软件架构设计与核心模块解析
硬件是骨架,软件是灵魂。一个好的软件架构能让代码清晰、易于调试和维护。对于这个项目,我强烈推荐采用“时间片轮询”或“简易状态机”的裸机架构,这比盲目上RTOS更简单可靠。
3.1 主程序框架:超级循环与时间片
核心是一个精心设计的main()函数超级循环,配合一个精准的定时器中断(如SysTick或通用定时器,中断周期1ms)来提供系统时基。
// 伪代码示例 volatile uint32_t g_sys_tick = 0; // 1ms递增的系统时钟 void SysTick_Handler(void) { // 1ms中断服务函数 g_sys_tick++; // 可以在这里放置需要严格定时执行的任务标志置位 } int main(void) { // 1. 初始化所有硬件:时钟、GPIO、定时器、中断、OLED、EEPROM等 Hardware_Init(); // 2. 从EEPROM加载系统参数和累计数据 Parameter_Load(); // 3. 显示开机画面,初始化状态 OLED_ShowWelcome(); State_Init(); while(1) { // 任务1:10ms执行一次,扫描按键 if (g_sys_tick - last_key_scan_tick >= 10) { Key_Scan_Task(); last_key_scan_tick = g_sys_tick; } // 任务2:50ms执行一次,更新显示(避免OLED刷新过快闪烁) if (g_sys_tick - last_disp_tick >= 50) { Display_Update_Task(); last_disp_tick = g_sys_tick; } // 任务3:100ms执行一次,核心计价逻辑计算 if (g_sys_tick - last_calc_tick >= 100) { Fare_Calculation_Task(); last_calc_tick = g_sys_tick; } // 任务4:500ms执行一次,闪烁提示或慢速任务 if (g_sys_tick - last_slow_tick >= 500) { LED_Blink_Task(); last_slow_tick = g_sys_tick; } // 其他即时性任务,如串口数据处理,可以放在这里直接调用 UART_Receive_Process(); } }这种架构清晰地将不同周期的任务分离,避免了在中断中处理复杂逻辑,也防止了某个任务阻塞整个系统。
3.2 里程测量模块:精准计数之道
里程测量是计价的核心输入,必须准确、抗干扰。推荐使用STM32定时器(如TIM2/TIM3/TIM4)的外部时钟模式1或编码器模式来捕获霍尔传感器的脉冲。
- 配置步骤:
- 将连接霍尔传感器的GPIO引脚配置为定时器的外部触发输入(ETR)或通道输入。
- 初始化定时器为外部时钟模式1,触发边沿选择上升沿或下降沿。
- 开启定时器更新中断,或者不中断,直接读取计数器值。
- 在中断或主循环中,定时(如每秒)读取定时器计数器值
CNT,这个值就是这段时间内捕获的脉冲数。将其清零后继续计数。 - 根据“脉冲数-里程”换算系数,计算出行程增量。
实操心得:务必在传感器信号输入到单片机引脚前,做好硬件滤波(RC低通滤波)和整形(施密特触发器)。软件上,可以在中断服务函数里只做简单的计数累加,将复杂的里程换算和计费逻辑放到主循环的任务中,这是保证系统实时性的关键。
3.3 计价逻辑实现:状态机是绝配
计价过程本身就是一个典型的状态机,至少包含“空车”、“载客(行驶)”、“载客(等待)”等状态。
typedef enum { STATE_FREE, // 空车状态,显示“空车”标志 STATE_RUNNING, // 载客行驶状态,按里程和时长计费 STATE_WAITING, // 载客等待状态(如堵车),按等待时间计费 STATE_PAUSE // 暂停计费(如司机手动暂停) } fare_state_t; // 计价参数结构体 typedef struct { float start_price; // 起步价 float start_distance; // 起步里程(公里) float price_per_km; // 超起步里程后,每公里单价(白天) float price_per_km_night; // 夜间每公里单价 float price_per_minute; // 等待时间单价(元/分钟) uint8_t is_night_mode; // 是否夜间模式 } fare_parameter_t;核心计价函数Fare_Calculation_Task()的逻辑:
- 判断状态:根据按键输入切换到相应状态。
- 数据采集:获取当前总里程脉冲数、当前时间(从RTC或系统滴答定时器获取)。
- 费用计算:
- 如果状态是
STATE_RUNNING:- 总行驶里程 = 脉冲总数 × 脉冲距离系数。
- 如果里程 <= 起步里程,费用 = 起步价。
- 如果里程 > 起步里程,费用 = 起步价 + (里程 - 起步里程) × 当前单价(区分白天/夜间)。
- 同时,如果车速低于某个阈值(如5km/h)持续一段时间(如30秒),则自动切换到
STATE_WAITING状态。
- 如果状态是
STATE_WAITING:- 计算等待时间(分钟)。
- 等待费用 = 等待时间 × 等待时间单价。
- 总费用 = 行驶费用 + 等待费用。
- 如果状态是
- 数据更新:将计算出的实时金额、里程、时间等更新到显示缓冲区。
3.4 显示与存储模块
- 显示模块:编写一个
Display_Update_Task()函数,根据当前状态,将不同的信息格式化后调用OLED驱动库显示。例如,在载客状态显示“金额:XX.XX元”、“里程:X.XX公里”、“时间:XX:XX”,在空车状态显示“空车”、“总营收:XXX元”。 - 存储模块:对EEPROM(AT24Cxx)的操作要特别注意。
- 页写入:AT24C02每页8字节,跨页写入需要分两次操作。写入数据时,最好先检查目标地址是否跨页。
- 写入延迟:每次写操作后,需要延时5-10ms(查阅芯片手册)等待内部写周期完成,再进行下一次操作。盲目连续写入会导致失败。
- 数据校验:存储重要数据(如总营收)时,可以采用“数据+校验和(或CRC8)”的方式。读取时先校验,失败则使用默认值。
- 减少写操作:像实时里程这种频繁变化的数据,不要每次都写EEPROM,只在掉电风险高的时候(如停车状态改变)或定时(如每小时)备份一次关键数据。
4. 核心代码实现与关键函数剖析
这里我们深入几个最核心的函数,看看具体代码怎么写,以及为什么要这么写。
4.1 定时器捕获脉冲计数
我们以TIM2的通道1作为外部脉冲输入为例,采用外部时钟模式。
// tim2.c void TIM2_ETR_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0}; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 2. 配置PA0为浮空输入(TIM2_ETR引脚) GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStruct); // 3. 配置TIM2时基单元,预分频和周期值在这里不重要,因为时钟源是外部 TIM_TimeBaseInitStruct.TIM_Period = 0xFFFF; // 自动重装载值设为最大,防止溢出 TIM_TimeBaseInitStruct.TIM_Prescaler = 0; // 预分频为0,不分频 TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct); // 4. 配置TIM2为外部时钟模式1,触发源为ETR引脚,上升沿触发 TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); // 或者使用 TIM_ETRConfig(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0); // 然后 TIM_SelectInputTrigger(TIM2, TIM_TS_ETRF); // 5. 使能TIM2更新中断(可选,用于处理计数器溢出) TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); NVIC_EnableIRQ(TIM2_IRQn); // 6. 启动TIM2计数器 TIM_Cmd(TIM2, ENABLE); } // 获取当前脉冲计数值 uint32_t Get_Pulse_Count(void) { uint32_t count = TIM_GetCounter(TIM2); // TIM_SetCounter(TIM2, 0); // 如果需要清零,在主循环的任务里定时清零,不要在中断里频繁操作 return count; }4.2 计价状态机与费用计算
这是一个简化但完整的计价核心函数,在100ms任务中调用。
// fare.c static fare_state_t current_state = STATE_FREE; static fare_parameter_t param; static float total_distance_km = 0.0; static float total_fare = 0.0; static uint32_t waiting_start_tick = 0; static uint32_t last_pulse_count = 0; void Fare_Calculation_Task(void) { uint32_t current_pulse; float delta_distance; uint32_t current_tick = g_sys_tick; switch(current_state) { case STATE_FREE: // 空车状态,计价器清零,显示空车信息 total_distance_km = 0.0; total_fare = 0.0; last_pulse_count = Get_Pulse_Count(); // 记录初始脉冲,用于计算差值 break; case STATE_RUNNING: // 1. 计算新增里程 current_pulse = Get_Pulse_Count(); // 处理计数器溢出(0xFFFF -> 0) if(current_pulse < last_pulse_count) { delta_distance = ((0xFFFF - last_pulse_count) + current_pulse) * PULSE_TO_KM; } else { delta_distance = (current_pulse - last_pulse_count) * PULSE_TO_KM; } last_pulse_count = current_pulse; total_distance_km += delta_distance; // 2. 计算行驶费用 if(total_distance_km <= param.start_distance) { total_fare = param.start_price; } else { float beyond_distance = total_distance_km - param.start_distance; float current_unit_price = param.is_night_mode ? param.price_per_km_night : param.price_per_km; total_fare = param.start_price + beyond_distance * current_unit_price; } // 3. 判断是否进入等待状态(例如,最近5秒内里程增量极小) static uint32_t low_speed_start_tick = 0; if(delta_distance < LOW_SPEED_THRESHOLD) { // 例如 5秒内移动小于10米 if(low_speed_start_tick == 0) { low_speed_start_tick = current_tick; } else if((current_tick - low_speed_start_tick) > 30000) { // 持续30秒 current_state = STATE_WAITING; waiting_start_tick = current_tick; low_speed_start_tick = 0; } } else { low_speed_start_tick = 0; // 速度恢复,清零计时 } break; case STATE_WAITING: // 计算等待费用(每分钟计费一次) uint32_t waiting_seconds = (current_tick - waiting_start_tick) / 1000; // 毫秒转秒 uint32_t waiting_minutes = waiting_seconds / 60; static uint32_t last_charged_minute = 0; if(waiting_minutes > last_charged_minute) { // 每过一分钟,增加等待费 total_fare += param.price_per_minute; last_charged_minute = waiting_minutes; // 注意:这里需要将新的total_fare同步到显示和存储 } // 检查是否脱离等待状态(车辆移动) current_pulse = Get_Pulse_Count(); if(abs(current_pulse - last_pulse_count) > RESUME_THRESHOLD_PULSE) { current_state = STATE_RUNNING; last_charged_minute = 0; } break; default: break; } // 更新显示缓冲区中的数据 Update_Display_Buffer(total_fare, total_distance_km, current_state); }4.3 EEPROM参数存储与加载
以AT24C02为例,演示如何安全地读写参数结构体。
// eeprom.c #define EEPROM_PARAM_ADDR 0x00 // 参数存储起始地址 #define EEPROM_DATA_ADDR 0x40 // 营运数据存储起始地址 // 将参数结构体写入EEPROM void Save_Parameters_To_EEPROM(fare_parameter_t *param) { uint8_t buffer[sizeof(fare_parameter_t) + 1]; // 额外1字节存放校验和 uint8_t checksum = 0; uint8_t *p = (uint8_t*)param; // 1. 拷贝数据到缓冲区 for(int i=0; i<sizeof(fare_parameter_t); i++) { buffer[i] = p[i]; checksum ^= p[i]; // 简单的异或校验 } buffer[sizeof(fare_parameter_t)] = checksum; // 2. 写入EEPROM,注意页边界 uint16_t addr = EEPROM_PARAM_ADDR; for(int i=0; i<sizeof(buffer); i++) { I2C_EEPROM_WriteByte(addr++, buffer[i]); HAL_Delay(5); // AT24C02写周期延时,必须等待! // 更好的做法:检查ACK,或使用带轮询的写函数 } } // 从EEPROM读取参数结构体 uint8_t Load_Parameters_From_EEPROM(fare_parameter_t *param) { uint8_t buffer[sizeof(fare_parameter_t) + 1]; uint8_t checksum = 0; uint8_t *p = (uint8_t*)param; // 1. 从EEPROM读取数据 uint16_t addr = EEPROM_PARAM_ADDR; for(int i=0; i<sizeof(buffer); i++) { buffer[i] = I2C_EEPROM_ReadByte(addr++); } // 2. 校验数据 for(int i=0; i<sizeof(fare_parameter_t); i++) { checksum ^= buffer[i]; } if(checksum == buffer[sizeof(fare_parameter_t)]) { // 校验成功,拷贝数据 for(int i=0; i<sizeof(fare_parameter_t); i++) { p[i] = buffer[i]; } return 1; // 成功 } else { // 校验失败,加载默认参数 Set_Default_Parameters(param); return 0; // 失败 } }5. 调试技巧与常见问题实录
做嵌入式项目,调试时间往往比编码时间长。下面是我在多个STM32项目中总结的,针对这个计价器系统的调试经验和常见坑点。
5.1 硬件调试:先确保信号干净
- 脉冲信号测量:这是最容易出问题的地方。用示波器或逻辑分析仪,直接测量连接到单片机GPIO的霍尔传感器输出信号。理想情况应该是干净、陡峭的方波。如果看到波形有毛刺或缓慢上升/下降,说明硬件滤波没做好,必然导致计数不准。解决方法:在传感器输出端并联一个10k上拉电阻到VCC,并串联一个100Ω电阻和到地104电容组成低通滤波。
- 电源噪声:用万用表测量单片机VCC引脚电压,在电机(如果模拟车轮转动)启动瞬间,看电压是否有跌落。如果跌落超过0.2V,可能导致单片机复位。解决方法:在电源入口加大电容(如220uF电解电容并联104瓷片电容),并确保电源线足够粗。
- I2C通信失败:OLED或EEPROM不工作。首先检查硬件连接(SCL、SDA的上拉电阻4.7kΩ是否接好),然后用逻辑分析仪抓取I2C波形,看起始信号、地址、ACK是否正常。软件上,注意I2C时钟频率不要设太高,初期用100kHz比较稳妥。
5.2 软件调试:分层排查,善用工具
- 利用串口打印:这是最有效的调试手段。在代码关键位置(如状态切换、费用计算、脉冲计数时)通过串口打印变量值。例如:
使用printf("State: %d, Pulse: %lu, Dist: %.2fkm, Fare: %.2f\r\n", current_state, current_pulse, total_distance_km, total_fare);sprintf格式化到缓冲区,再发送,避免在中断中直接调用printf。 - STM32 CubeMonitor或SEGGER SystemView:如果你想深入了解任务执行时间和系统运行状态,这些实时监控工具非常强大。可以可视化地看到各个任务的执行时长和调度情况,找出潜在的性能瓶颈。
- 仿真调试:对于复杂的计价逻辑,可以在PC上先用C语言写一个简单的测试程序,模拟脉冲输入和状态变化,验证算法是否正确,然后再移植到单片机。
5.3 常见问题与解决方案速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 里程计数不准,时多时少 | 1. 传感器信号抖动(硬件消抖不足) 2. 定时器计数模式或中断配置错误 3. 脉冲换算系数计算错误 | 1. 用示波器看信号波形,加强RC滤波。 2. 检查定时器是否配置为外部时钟模式,确认触发边沿。 3. 实测轮胎周长和每圈脉冲数,重新计算 PULSE_TO_KM。 |
| 按键反应迟钝或连击 | 1. 软件消抖延时过长或逻辑错误 2. 按键扫描任务周期太长 3. 硬件上拉电阻未接或损坏 | 1. 采用“按下检测->延时10ms->再次检测”的标准消抖流程。 2. 确保按键扫描任务周期在10-20ms。 3. 检查原理图,确保GPIO模式为上拉/下拉输入,或外部有上拉电阻。 |
| OLED显示乱码或不显示 | 1. I2C地址错误 2. 初始化序列未正确发送 3. 电源不稳定 4. 刷新过快 | 1. SSD1306常见地址是0x78或0x7A,用逻辑分析仪确认。 2. 严格按照驱动芯片手册的初始化流程写代码。 3. 测量OLED模块VCC电压是否为3.3V或5V(视模块而定)。 4. 限制刷新频率,每秒20-30次足够。 |
| EEPROM数据偶尔丢失 | 1. 写操作后未等待足够时间(Twr) 2. 跨页写入未处理 3. 电源跌落导致写过程中断 | 1. 每次写字节后至少延时5ms。 2. 写函数内判断地址,如果跨页则分两次写。 3. 增加电源监控电路,或在检测到电压过低时禁止写操作。 |
| 计费金额跳变或计算错误 | 1. 浮点数计算精度或类型转换问题 2. 全局变量在中断和主循环中被同时访问(未保护) 3. 状态切换逻辑有漏洞 | 1. 尽量使用整数运算(如以“分”为单位存储金额)。如需浮点,用float,注意强制转换。2. 对共享的关键变量(如 total_fare),在中断中只置标志,在主循环中处理;或使用关中断/调度器锁进行保护。3. 画出详细的状态转换图,检查每个转换条件是否严密。 |
| 系统运行一段时间后死机 | 1. 堆栈溢出 2. 中断服务函数执行时间过长 3. 内存泄漏(如果用了malloc) 4. 看门狗未喂狗(如果启用) | 1. 在启动文件或链接脚本中适当增大堆栈大小。 2. 中断里只做最紧急的事(如置标志、清中断),耗时操作放到主循环。 3. 嵌入式项目尽量避免动态内存分配。 4. 检查看门狗配置和喂狗逻辑。 |
5.4 毕设答辩与文档准备心得
最后,从毕设答辩的角度提几点建议:
- 演示稳定性是关键:准备一个能稳定运行5-10分钟的完整演示。可以用一个小电机带动磁铁旋转模拟车轮,用手按按键模拟交互。确保演示过程中不死机、不显示乱码。
- 讲清楚设计思路:不要只演示功能。在PPT和讲解中,要突出你的系统设计思路:为什么选这个单片机?为什么用状态机?如何保证计费准确性和数据可靠性?这体现了你的工程思维。
- 展示核心代码:准备一两页最核心的代码片段,如状态机切换、计价函数、EEPROM存储,并简要解释其逻辑。
- 准备问题:提前思考评委可能问的问题,例如:“如何应对传感器干扰?”、“如果要求增加GPS模块记录轨迹,你的系统架构如何扩展?”、“你的计费算法复杂度是多少?有没有优化空间?”
- 文档齐全:除了论文,提供清晰的原理图、PCB图(如果做了)、程序流程图、核心代码注释、使用说明书。这会让你的项目显得非常专业和完整。
这个项目做下来,你对STM32的理解绝不会再停留在点灯和串口。你会真正触摸到一个完整产品从需求到实现的全过程,尤其是对实时性、可靠性的考量,这是书本上学不到的宝贵经验。
本文还有配套的精品资源,点击获取