简介:这套STM32毕业设计集合源码与配套资料面向正在准备或推进STM32微控制器相关毕业设计的学生,覆盖从硬件电路到软件程序的完整链路,适合需要快速搭建项目框架、理解开发流程的初中级嵌入式学习者。压缩包共26个文件、约8.09MB,内含C源码与Keil工程文件(uvproj、hex、lst)、原理图与PCB设计文件(sch、pcb及其预览)、元件清单xls、参考论文doc/docx、PDF说明及操作视频索引txt,基本对应一个完整设计项目从选型、画板、编程到成文的主要环节。已有7344人学习,说明其在毕业设计场景中具备一定参考价值。读者可据此对照原理图理解外设连接,借助源码分析初始化、中断、定时器、串口与ADC等实现,并按元件清单核算成本;参考论文部分还能补充低功耗、RTOS集成及应用案例的理论背景,便于查漏补缺与独立排查问题。
1. 从一堆没法继续的工程开始:这套 STM32 毕业设计源码合集能解决什么
板子到手第二周,开题报告还压在桌上,资料包里十几个 Keil 工程,点开一个就是满屏 error,连 main 在哪都要翻半天——这大概是做 STM32 毕业设计最常见的开局。这套 STM32 毕业设计源码加资料合集针对的就是这个阶段:按题目方向分类的完整工程、配套原理图和器件手册、可参考的论文框架,覆盖智能家居、环境监测、电机控制、红外遥控、无线通信等常见选题方向。
它不会教你 C 语言指针,也不会替你答辩,但能提供一块能跑起来的底板。有了这块底板,你就不用再花三周去研究时钟树怎么配、串口为什么乱码,而是可以把时间放在自己的功能点上。适合三类人:第一次接触 Cortex-M3 的学生、跨专业想快速搭出原型的自学者,以及需要给答辩准备多套备选方案的小团队。软件层面该装的 Keil、CubeMX 以及常用插件,资料里一般都有清单说明,照着安装就行。
2. 技术栈与目录结构:先理清这套合集的骨架再动手
拿到一个几十兆的压缩包,很多人的第一反应是找“最新”或者“最全”的那个文件夹,然后随便挑一个工程打开就编译。这一步一旦走错,后面基本就是在跟编译器报错较劲。更合理的顺序是:先判断代码属于哪套库,再看目录怎么分层,最后确认工程配置和自己手上的芯片对不对得上。这三件事搞清楚,后面百分之八十的奇怪问题都不会出现。
2.1 标准库、HAL 库与 RTOS:三套代码风格怎么选
合集里的工程大致分三类。第一类是标准外设库(SPL),典型特征是stm32f10x_gpio.c、stm32f10x_rcc.c这一堆文件,main里全是RCC_APB2PeriphClockCmd、GPIO_Init。这类代码在 F103 上最常见,逻辑直白,寄存器层面的东西看得见,答辩时被问到“这个时钟怎么开的”能答上来,很多导师也认这套。第二类是 HAL 库配 CubeMX,特征是HAL_GPIO_Init、MX_GPIO_Init,工程里有个.ioc文件。改引脚、加外设只要在图形界面点几下重新生成,适合功能点多的题目。第三类是带 RTOS 的,FreeRTOSConfig.h、xTaskCreate一出现,说明作者已经把功能拆成任务了。
我一般这样决策:如果题目偏底层、老师要求讲清楚寄存器,就采用标准库那一批;如果题目是“多传感器采集 + 屏幕显示 + 无线上报”这种任务并发的场景,HAL 加 FreeRTOS 会省很多事。需要提醒的是,同一个合集里两套库混着用,会踩到最典型的问题——HAL 工程里手动贴了一段标准库代码,结果RCC_APB2PeriphClockCmd未定义,或者反过来HAL_Init()没调,时钟树停留在 8MHz 内部 RC,串口自然全是乱码。选一套,从头到尾用一套。
另外,不同工程里的“公共层”写法差别很大:有的把延时写成delay_ms循环空转,有的用 SysTick 做非阻塞计时。判断标准很简单,看main的 while 循环里有没有delay_ms(1000)这种写法——有的话就是阻塞式,后面加功能时得自己重构成状态机,否则屏幕刷新和按键响应一定会互相拖累。
2.2 目录分层:源码、原理图、文档怎么对上
一份整理得比较好的合集,目录大致是这样分层的,拿到手先按这张表过一遍,比盲目点开工程快得多:
| 目录/文件 | 里面通常是什么 | 怎么用 |
|---|---|---|
01_源码/ | 按题目分的 Keil 工程,每个一个文件夹 | 先看文件夹名,挑和自己题目最接近的 |
02_原理图/ | PDF 或图片格式的板子原理图、PCB 截图 | 对照引脚,确认代码里的 PB5 到底接了什么 |
03_器件手册/ | 传感器、显示屏、无线模块的 datasheet | 查寄存器地址、时序、供电电压 |
04_文档/ | 开题、任务书、论文框架、答辩 PPT 模板 | 只借结构和章节,内容必须自己写 |
05_工具/ | 下载器驱动、串口助手、烧录软件 | 装驱动时用,注意版本 |
README或说明 | 每个工程的适用芯片、注意事项 | 优先读,能省掉一半试错 |
原理图这一层特别容易被跳过。血泪经验是:代码里写GPIO_Pin_5,你以为接的是 LED,实际上原理图上 PB5 接的是蜂鸣器,一上电就一直响。还有一类情况是工程用的开发板和手上的板子型号不同,LED 极性反了,代码里GPIO_ResetBits是点亮,你照着写成SetBits,结果灯永远不亮,还以为是时钟没开。花十分钟把原理图和代码里的引脚宏对一遍,比调两小时强。
2.3 工程与芯片型号对齐:Keil、CubeMX 三条路都绕不开的三处配置
芯片型号对不上,是新手最容易翻车的地方。F103 系列内部还分小容量、中容量、大容量,对应启动文件和宏定义都不一样:中容量(如 C8T6)要定义STM32F10X_MD,启动文件用startup_stm32f10x_md.s;大容量(如 ZET6)是STM32F10X_HD配startup_stm32f10x_hd.s。工程里这两个对不上,典型现象是编译能过,下载进去不跑,或者一进中断就 HardFault。
第二处是外部晶振。代码里stm32f10x.h中的HSE_VALUE默认 8000000,板子上焊的是 8MHz 就没事,如果换成 12MHz 的板子而没改这个宏,串口波特率会整体偏掉,打印出来就是一堆问号。第三处是下载算法,Keil 的 Options → Debug → Settings → Flash Download 里要选对 Flash 大小,选错了会提示 “Cannot access target” 或者下载到一半失败。
用 CubeMX 的工程还有一条:.ioc文件里的芯片型号必须和实际一致,否则生成的引脚初始化代码会引用不存在的资源。有人图省事直接在 CubeMX 里选了同系列更高配的型号,编译没问题,跑起来外设行为诡异——因为时钟使能位根本不是一回事。
注意:把工程从合集里复制出来之后,路径里不要有中文和空格。Keil 对中文路径的支持时好时坏,报错信息还特别有迷惑性,经常提示找不到某个
.h,实际原因就是路径。
3. 环境搭建与第一个工程跑通:从装驱动到串口出字
真正让人有成就感的时刻,是串口助手上跳出第一行字。为了这一行字,需要把工具链装全、把工程配置对、把下载链路打通。这一章按顺序走一遍,每一步都会说清楚失败时该看哪里,而不是只给一条命令。
3.1 工具链与驱动:装对这四样就够了
最小可用的组合是:Keil MDK(5.x 版本即可,不必追新)、对应芯片的 Device Family Pack、ST-Link 或 J-Link 驱动、CH340 串口驱动。装完 Keil 之后第一件事是打开 Pack Installer,确认Keil::STM32F1xx_DFP已经装好,否则新建工程时器件列表是空的。ST-Link 驱动装完,设备管理器里应该能看到 “STMicroelectronics STLink dongle”,看不到就换根数据线——很多所谓“下载器坏了”的问题,实际是线只能供电不能传数据。
代码风格上如果要看格式化,可以装个 AStyle 插件;要快速生成初始化代码就把 STM32CubeMX 挂上。插件属于锦上添花,先保证编译器和下载器能用。我见过有人花一下午折腾插件,结果工程连core_cm3.h都找不到,问题其实出在器件支持包没装。
装完之后建议立刻做一次空工程验证:新建工程、选芯片、不加任何代码、直接编译。能过说明工具链没问题,后面出问题就只可能是工程本身的配置。
3.2 编译、下载、复位:三步里最常见的卡点
打开目标工程后,先看 Options for Target 的 Device 页芯片型号,再点 Build。第一次编译大概率会报头文件找不到,这时去 C/C++ 页的 Include Paths 里把CMSIS、StdPeriph_Driver/inc、User这几个目录补进去。编译通过之后开始下载:Debug 页选 ST-Link Debugger,Settings 里 Port 选 SW,能读到 IDCODE 说明连接正常;Flash Download 页勾上 Reset and Run,下载完自动跑。
下载成功但不运行的经典原因有三个:BOOT0 跳线帽没接地(芯片进了 ISP 模式),代码里时钟初始化卡在while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET)(外部晶振没起振,焊点虚了或者负载电容不对),以及中断向量表偏移没设对(用了 IAP 或者带 bootloader 的板子才会遇到)。前两个占九成,先量晶振有没有波形,再查跳线帽。
# 用命令行烧录时(ST-Link 官方工具)常见的一条,确认能连上再谈其他 ST-LINK_CLI.exe -c SWD UR -P build/app.hex -V -Rst这条命令里-c SWD UR指定 SWD 接口和复位方式,-P指定要烧录的文件,-V是烧录后校验,-Rst是烧完复位运行。图形界面点多了容易忘配置,命令行反而更容易固定流程。如果你用的是串口 ISP 下载(FlyMcu 一类),记得先按着 BOOT0 上电,波特率先用 115200 试,不行再降到 9600。
3.3 用串口打印确认程序真的在跑
灯亮不代表程序在跑,可能只是上电默认电平。最可靠的验证是串口打印。标准库工程里加下面这段,把printf重定向到 USART1:
/* usart.c —— printf 重定向到 USART1,Keil 里必须勾选 Use MicroLIB */ #include "stm32f10x.h" #include <stdio.h> void USART1_Init(uint32_t baud) { GPIO_InitTypeDef gpio; USART_InitTypeDef uart; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); 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); uart.USART_BaudRate = baud; uart.USART_WordLength = USART_WordLength_8b; uart.USART_StopBits = USART_StopBits_1; uart.USART_Parity = USART_Parity_No; uart.USART_HardwareFlowControl = USART_HardwareFlowControl_None; uart.USART_Mode = USART_Mode_Tx | USART_Mode_Rx; USART_Init(USART1, &uart); USART_Cmd(USART1, ENABLE); } int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); /* 等发送完成,否则会丢字符 */ USART_SendData(USART1, (uint8_t)ch); return ch; }USART1_Init里的参数含义很直接:USART_BaudRate是波特率,要和串口助手一致;USART_Mode同时开收发,只发不收也可以只写USART_Mode_Tx。fputc是 C 库的输出钩子,重定向之后printf("hello\r\n")就会从 PA9 出来。这里有两个必须注意的点:一是在 Keil 的 Target 页勾选 Use MicroLIB,否则链接阶段会报printf相关错误;二是while里等的是USART_FLAG_TC(发送完成),等TXE也行,但连续的字符容易粘包。
串口助手那边设 115200、8 位数据、1 位停止、无校验,接收区应该能看到周期性输出。如果全是乱码,按这个顺序查:波特率是否一致、HSE_VALUE和实际晶振是否匹配、GND 是否共地。三条都排除了还是乱码,那多半是时钟配置本身有问题,回到 3.2 去量晶振。
提示:把
printf放进 1ms 定时中断里会严重拖慢中断响应,调试信息尽量在主循环里打,或者用环形缓冲把数据攒起来再一次性发。
4. 核心模块拆解:从点灯到通信的四层实现
合集里的工程无论题目多花哨,剥掉应用逻辑之后都是同一套东西:GPIO 打底、定时器提供时间基准和波形、I2C/SPI 挂传感器和屏幕、串口负责和外界说话。把这四层看懂,任何一个工程拿过来都能在半天内理清主线。
4.1 GPIO 与按键:最不起眼却最容易出问题的一层
点灯是入门第一课,但按键扫描写不好,整套交互都会受影响。合集里常见的写法有两种:主循环里delay_ms(10)消抖,或者定时器里每 10ms 采一次做状态机。前者简单但阻塞,后者多几行代码却能立刻响应。
/* key_led.c —— 标准库写法,F103 通用 */ #include "stm32f10x.h" void LED_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); /* 漏了这句,后面全白干 */ gpio.GPIO_Pin = GPIO_Pin_13; gpio.GPIO_Mode = GPIO_Mode_Out_PP; /* 推挽输出 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &gpio); GPIO_SetBits(GPIOC, GPIO_Pin_13); /* 很多板子低电平点亮,这里先置高灭掉 */ } void KEY_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0; gpio.GPIO_Mode = GPIO_Mode_IPU; /* 上拉输入,按键另一端接地 */ GPIO_Init(GPIOA, &gpio); } uint8_t KEY_Scan(void) { static uint8_t pressed = 0; if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) { /* 按下为低 */ if (!pressed) { pressed = 1; return 1; } /* 只在下降沿报一次,避免连击 */ } else { pressed = 0; } return 0; }GPIO_Mode_IPU是内部上拉输入,省掉外部电阻;如果板上已经有上拉,用GPIO_Mode_IN_FLOATING也行。pressed这个静态变量是核心,它保证按住不放只算一次按下,松手才复位。少了它,主循环转一圈就报一次,菜单会疯狂跳页,这也是学生答辩时最常见的“演示故障”。
注意:配置成
GPIO_Mode_AF_PP的引脚不能再当普通 IO 用,标准库不会报错,但读回来永远是 0。PWM 引脚要读状态,得另想办法或者换引脚。
4.2 定时器与 PWM:从呼吸灯到电机调速
定时器是 STM32 里最值得花时间的一块。PWM 输出的占空比计算,本质是两个数:预分频Prescaler决定计数频率,重装载Period决定周期。以 72MHz 主频为例,预分频写 71 得到 1MHz 计数频率,Period写 999 就是 1ms 周期、1kHz 的 PWM,比较值Pulse直接对应占空比千分比。
/* pwm.c —— TIM3_CH2 输出到 PB5,驱动 LED 或舵机 */ void TIM3_PWM_Init(uint16_t period, uint16_t duty) { TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; GPIO_InitTypeDef gpio; uint16_t psc = 72 - 1; /* 72MHz / 72 = 1MHz 计数频率 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); gpio.GPIO_Pin = GPIO_Pin_5; /* TIM3_CH2 默认映射在 PB5 */ gpio.GPIO_Mode = GPIO_Mode_AF_PP; /* 写成普通输出,波形出不来 */ gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &gpio); tb.TIM_Period = period - 1; tb.TIM_Prescaler = psc; tb.TIM_ClockDivision = TIM_CKD_DIV1; tb.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &tb); oc.TIM_OCMode = TIM_OCMode_PWM1; oc.TIM_OutputState = TIM_OutputState_Enable; oc.TIM_Pulse = duty; /* 占空比 = duty / period */ oc.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC2Init(TIM3, &oc); TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM3, ENABLE); }函数参数period和duty都用计数值而不是百分比,是为了免去浮点运算,在中断里也能直接改。运行中调速只需调用TIM_SetCompare2(TIM3, new_duty),一行就能改占空比,不需要重新初始化。舵机场景要特别小心:常见舵机要求 50Hz、0.5ms 到 2.5ms 脉宽,所以period得设成 20000(20ms),duty在 500 到 2500 之间变化,直接用 1kHz 的配置会让舵机抖得像筛糠。
4.3 I2C 与传感器读取:软件模拟为什么更受欢迎
合集里大量工程用的是软件模拟 I2C,而不是硬件 I2C 外设。原因很实际:硬件 I2C 在 F103 上历史遗留问题多,总线锁死、从机不应答导致死循环的情况时有发生;软件模拟改引脚只要改两个宏,换板子不用重新查复用表。代价是占用 CPU,但在 100kHz 这个速率下,读一个温度值完全够用。
/* soft_i2c.c —— 引脚可改,SDA 必须配成开漏输出 */ #define SCL_H() GPIO_SetBits(GPIOB, GPIO_Pin_6) #define SCL_L() GPIO_ResetBits(GPIOB, GPIO_Pin_6) #define SDA_H() GPIO_SetBits(GPIOB, GPIO_Pin_7) #define SDA_L() GPIO_ResetBits(GPIOB, GPIO_Pin_7) #define SDA_READ GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) static void i2c_delay(void) { volatile uint8_t i = 20; /* 空转延时,太快从机直接不响应 */ while (i--); } void I2C_Start(void) { SDA_H(); SCL_H(); i2c_delay(); SDA_L(); i2c_delay(); /* SCL 高电平期间 SDA 下降沿 = 起始位 */ SCL_L(); } uint8_t I2C_WriteByte(uint8_t dat) { uint8_t i, ack; for (i = 0; i < 8; i++) { if (dat & 0x80) SDA_H(); else SDA_L(); dat <<= 1; i2c_delay(); SCL_H(); i2c_delay(); SCL_L(); } SDA_H(); /* 释放 SDA,让从机拉低应答 */ i2c_delay(); SCL_H(); i2c_delay(); ack = SDA_READ; /* 0 表示从机应答成功 */ SCL_L(); return ack; }i2c_delay里的空转次数决定总线速率,20 次大概在 100kHz 上下,别写太小。I2C_WriteByte返回的ack是判断器件在不在的关键:返回 1 说明没人应答,要么地址错了,要么线没接好。常见 OLED 模块的 I2C 地址有0x78和0x7A两种写法(7 位地址 0x3C 左移一位),读不到画面先扫一遍地址,比反复改代码有效。
SDA 引脚初始化必须写成GPIO_Mode_Out_OD(开漏输出),这样在写 1 时能被从机拉低;写成推挽输出,应答位永远读不到。这是软件 I2C 最经典的坑,很多人卡在这里以为是模块坏了。
4.4 串口协议与数据帧:和上位机对接的约定
传感器的值要送到电脑或者另一个模块,就得定一个帧格式。最简单的约定是:帧头0xAA 0x55、长度、命令字、数据区、校验和。接收端用状态机逐字节喂,校验通过才分发,比一股脑塞进缓冲区再找帧头稳得多。
/* protocol.c —— 逐字节状态机,放在 USART 中断里调用 */ typedef enum { WAIT_H1, WAIT_H2, WAIT_LEN, WAIT_CMD, WAIT_DATA, WAIT_SUM } pstate_t; static pstate_t state = WAIT_H1; static uint8_t buf[32], len, idx, cmd, sum; void Protocol_Feed(uint8_t byte) { switch (state) { case WAIT_H1: if (byte == 0xAA) state = WAIT_H2; break; case WAIT_H2: state = (byte == 0x55) ? WAIT_LEN : WAIT_H1; break; case WAIT_LEN: len = byte; idx = 0; sum = byte; state = (len > 0 && len <= sizeof(buf)) ? WAIT_CMD : WAIT_H1; break; case WAIT_CMD: cmd = byte; sum += byte; state = WAIT_DATA; break; case WAIT_DATA: buf[idx++] = byte; sum += byte; if (idx >= len) state = WAIT_SUM; break; case WAIT_SUM: if (byte == sum) Protocol_Handle(cmd, buf, len); /* 校验过了才处理 */ state = WAIT_H1; break; default: state = WAIT_H1; } }每个状态只做一件事:WAIT_H1找第一个帧头,WAIT_H2确认第二个,WAIT_LEN取长度并做合法性检查(超过buf大小就丢弃重来,防止缓冲区溢出),WAIT_SUM比对校验和。sum用的是简单累加和,够用且计算量小;要求高一点可以换成 CRC16,代码多几十行。这个结构的好处是哪怕中间丢了几个字节,状态机也能自己回到找帧头的状态,不会卡死。
发送端对应地把数据按同样格式拼好再printf或者用 DMA 发出去。上位机那边用串口助手加个自定义协议解析,或者干脆用 Python 的pyserial写十几行脚本,画个实时曲线,答辩演示的视觉效果比数字滚动强不少。
5. 避坑与排查:让答辩现场出故障的五个问题
调试阶段踩的坑大多有共性,下面这几条是我在帮人看工程时反复遇到的,按“现象 → 原因 → 解决”记下来,出问题时对照着查能省掉大量时间。
现象一:编译报cannot open source input file "core_cm3.h",或者一堆外设函数未定义。原因通常是器件支持包没装,或者 Include Paths 里少了CMSIS和StdPeriph_Driver/inc目录;也有一种情况是工程从别的电脑拷过来,原来的绝对路径失效了。解决方法是打开 Pack Installer 确认 DFP 已安装,然后在 C/C++ 页把相对路径手动补上,路径统一用..\Libraries\CMSIS这种形式,避免换电脑后再次失效。
现象二:Keil 提示下载成功,板子毫无反应,按复位也没用。先查 BOOT0 跳线,悬空或接高会进 ISP 模式,程序根本不执行;再查 Options → Debug → Settings 里 Flash Download 的算法是否选了对应容量,选错会写到不存在的地址;最后确认代码里有没有卡在while(RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET),外部晶振不起振时程序就停在这。用示波器量一下晶振引脚,没有波形就补焊或者换负载电容。
现象三:串口输出乱码,或者只出第一个字符后面全丢。乱码九成是波特率或时钟不对:HSE_VALUE和实际晶振不一致、串口助手设错了波特率、没共地。只出第一个字符通常是fputc里等的是USART_FLAG_TXE而不是TC,或者没有勾选 Use MicroLIB,重定向函数压根没生效。还有一类是中断里调printf导致重入,改成主循环打印或者加环形缓冲。
现象四:OLED 或传感器读不到数据,I2C_WriteByte返回 1。依次查三件事:SDA 是否配置成开漏输出、总线上有没有 4.7k 上拉电阻、器件地址对不对。很多模块自带上拉,但排针线太长导致上升沿变缓,把i2c_delay调大一点就能通。另外注意供电,3.3V 器件接 5V 有时能亮但通信不稳,属于玄学故障,量一下电压最实在。
现象五:加了新功能之后,程序跑几分钟就死机或者自动重启。自动重启基本是独立看门狗没喂,检查主循环里有没有阻塞超过喂狗周期;死机则优先怀疑 HardFault,常见原因是数组越界写坏了栈、中断里用了浮点运算、结构体指针未初始化。定位方法是在 HardFault_Handler 里加一句打印或者点亮一个灯,再用调试器看调用栈,比盲猜快得多。另外全局变量在中断和主循环里同时读写,记得加volatile,否则编译器优化之后行为完全不可预测。
6. 二次开发改造:把合集里的工程变成自己的毕设
直接拿一个现成工程去答辩,风险不在于查重,而在于老师问一句“这部分为什么这么写”就答不上来。我的改造思路是先做分层,再加一个属于自己的功能点。分层指的是把代码拆成bsp(板级驱动,管引脚和寄存器)、app(业务逻辑,管流程)、main(只做初始化和调度)三层,原工程里那些混在main.c里的传感器读取代码全部挪进app。这一步做完,代码量看着没变,但可读性完全不同,答辩讲起来也有条理。
加功能点建议从“数据 + 判断 + 反馈”这个链条入手,成本低又能讲故事。比如原来只做温湿度采集,就加一个阈值判断和蜂鸣器报警,再加一条历史数据写入 Flash 的曲线;原来是按键控制 LED,就改成状态机驱动的多级菜单,配 OLED 显示。这里有个小技巧:把传感器抽成统一接口,换型号时只加一个文件,不动主逻辑。
/* app_sensor.h —— 统一传感器接口,换型号只加实现文件 */ typedef struct { const char *name; uint8_t (*init)(void); uint8_t (*read)(float *value); /* 返回 0 表示读取成功 */ uint16_t period_ms; /* 采样周期,调度器按这个轮询 */ } sensor_t; /* app_sensor.c 里的注册表,新增传感器只要在这里加一行 */ static const sensor_t sensors[] = { { "SHT30", sht30_init, sht30_read, 1000 }, { "BH1750", bh1750_init, bh1750_read, 500 }, };这个结构体数组的价值在于,主循环变成一个统一的轮询器:按period_ms判断哪个传感器该采了,调用read拿值,失败就重试两次再报错。加传感器不用改调度逻辑,答辩时被问“扩展性怎么样”,这段代码就是答案。改造后的验证方法也要具体:先用串口把每个传感器的原始值打出来,和手册上的量程对一遍;再用逻辑分析仪抓 I2C 波形,确认时序和地址;最后拔掉一个传感器,看程序是否会卡死,正常的实现应该是标记故障继续跑其他任务。
改造前后可以做个简单对照,方便自己评估工作量:
| 维度 | 原工程常见状态 | 改造后目标 |
|---|---|---|
| 代码组织 | 全部堆在main.c | bsp / app / main 三层 |
| 延时方式 | delay_ms阻塞 | 定时器节拍 + 状态机 |
| 传感器接入 | 直接写在主循环 | 统一接口 + 注册表 |
| 故障处理 | 读失败就死等 | 超时重试 + 故障标记 |
| 调试手段 | 只看灯 | 串口日志 + 波形 |
从那以后我每次接手别人的工程,第一件事都是先把它编译通过、串口打出一行字,再动任何一行业务代码——这个习惯救过我很多次,因为一旦改坏了,至少知道问题出在自己改的地方。希望帮到你。
本文还有配套的精品资源,点击获取