作为一名电子科学与技术专业的本科生,毕业设计是检验我们四年所学、将理论转化为实践的关键一环。然而,很多同学在起步阶段就遇到了难题:选题要么过于宏大空洞,要么缺乏工程实现的清晰路径;面对硬件选型、软件编程和系统联调,常常感到无从下手。今天,我就以“从零构建一个嵌入式信号采集系统”为例,分享我的毕设实战经验,希望能为同样迷茫的你提供一份清晰的“施工图”。

嵌入式开发板与传感器

1. 毕设常见痛点与破局思路

在开始技术细节前,我们先梳理几个普遍存在的痛点,并给出对应的解决思路。

  1. 选题空泛,缺乏边界:例如“基于物联网的环境监测系统”,这个题目范围太大,涉及传感器、嵌入式、通信协议、云平台、前端展示等多个领域,对于本科毕设而言难以深入。更好的做法是聚焦核心,如“基于STM32的温湿度数据高精度采集与本地存储系统”,目标明确,易于实现和评估。
  2. 软硬件割裂,缺乏系统思维:很多同学硬件画完板子就交给软件同学,或者软件只关心逻辑,不考虑硬件时序和资源限制。嵌入式系统的精髓在于“协同”,必须从系统层面考虑功耗、实时性、通信带宽和存储容量。
  3. 调试能力薄弱,问题定位困难:当系统不工作时,面对硬件电路和数千行代码,容易陷入盲目尝试。建立模块化的调试方法和有效的日志输出机制至关重要。

针对这些痛点,本次分享的项目——嵌入式信号采集系统,就是一个理想的练手项目。它涵盖了信号感知、数据处理、通信传输等核心环节,体量适中,且极易扩展。

2. 技术选型:平衡性能、复杂度与学习成本

技术选型决定了项目的开发难度和最终性能。这里对几个关键选择进行对比分析。

  1. 主控MCU选型:STM32 vs Arduino

    • Arduino (AVR/ESP系列):优势在于生态丰富、库函数完善、上手极快,适合快速验证想法。但其硬件抽象层较高,不利于深入理解底层寄存器操作、中断管理和电源模式,性能也相对有限。
    • STM32 (ARM Cortex-M系列):这是工业界的主流选择。它提供了从M0到M7不同性能的芯片,资源丰富(多路ADC、DMA、丰富的外设)。使用HAL库或LL库可以平衡开发效率与底层控制能力。对于毕设而言,选择STM32F1或F4系列是更佳选择,既能深入理解嵌入式核心概念,又拥有强大的社区支持。

    结论强烈推荐STM32。毕设不仅是完成功能,更是学习过程。掌握STM32开发,对你未来的求职或深造都更有价值。

  2. 传感器通信接口:I2C vs SPI vs UART vs 模拟信号

    • 模拟信号 (ADC采样):最直接的方式,如采集热敏电阻的电压。需要关注信号调理(放大、滤波)、ADC精度和采样率。优点是电路直观,缺点是易受噪声干扰。
    • I2C:两根线(SDA, SCL),支持多设备,但速度较慢(标准模式100kbps)。适合连接温湿度传感器(如SHT30)、气压计等对速度要求不高的设备。
    • SPI:全双工,四线制,速度非常快(可达数十Mbps)。适合连接高速ADC芯片、Flash存储器等。时序要求严格。
    • UART:异步串口,简单可靠,常用于传感器模组(如GPS模块、某些气体传感器)或与上位机(PC)通信。

    结论:本项目核心是信号采集,因此重点使用MCU内部ADC采集模拟信号,同时可以拓展一个I2C数字传感器(如温湿度)来丰富数据维度。与上位机通信则使用UART,因其在PC端对接最简单。

3. 核心实现细节与代码剖析

系统工作流程:传感器模拟信号 -> 信号调理电路 -> STM32 ADC采样 -> DMA传输至内存缓冲区 -> 数据处理(如软件滤波) -> 封装成帧 -> UART发送至上位机。

  1. ADC采样与DMA传输 直接使用查询或中断方式读取ADC数据在高速采样时会导致CPU负载过重。使用**DMA(直接存储器访问)**是标准做法。ADC配置为连续扫描模式,DMA配置为循环模式,将ADC转换结果自动搬运到指定的数组缓冲区中,完全无需CPU干预。

    // 示例:ADC1 通道1 使用DMA进行连续采样
    #define ADC_BUFFER_SIZE 1024
    uint16_t adc_buffer[ADC_BUFFER_SIZE]; // DMA目标缓冲区
    
    void ADC_DMA_Init(void) {
        // 1. 启用ADC1、DMA、GPIO时钟
        __HAL_RCC_ADC1_CLK_ENABLE();
        __HAL_RCC_DMA2_CLK_ENABLE();
        __HAL_RCC_GPIOA_CLK_ENABLE();
    
        // 2. 配置ADC引脚(PA1)为模拟输入
        GPIO_InitTypeDef GPIO_InitStruct = {0};
        GPIO_InitStruct.Pin = GPIO_PIN_1;
        GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
        HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
        // 3. 配置ADC参数
        ADC_HandleTypeDef hadc1;
        hadc1.Instance = ADC1;
        hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; // 单通道,不扫描
        hadc1.Init.ContinuousConvMode = ENABLE;     // 连续转换
        hadc1.Init.DiscontinuousConvMode = DISABLE;
        hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
        hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
        hadc1.Init.NbrOfConversion = 1;
        HAL_ADC_Init(&hadc1);
    
        // 4. 配置ADC通道
        ADC_ChannelConfTypeDef sConfig = {0};
        sConfig.Channel = ADC_CHANNEL_1; // 对应PA1
        sConfig.Rank = 1;
        sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES;
        HAL_ADC_ConfigChannel(&hadc1, &sConfig);
    
        // 5. 配置DMA
        DMA_HandleTypeDef hdma_adc1;
        hdma_adc1.Instance = DMA2_Stream0; // 根据芯片型号查数据手册确定Stream和Channel
        hdma_adc1.Init.Channel = DMA_CHANNEL_0;
        hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
        hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
        hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
        hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
        hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
        hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式,缓冲区满了自动从头开始
        hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
        HAL_DMA_Init(&hdma_adc1);
        __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); // 关联ADC和DMA
    
        // 6. 启动DMA传输
        HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE);
    }
    
  2. 数据缓存与双缓冲区策略 DMA在不断向adc_buffer写入数据。如果直接在adc_buffer上进行处理(如滤波、封装),可能会和处理中的旧数据冲突。一个经典的解决方案是双缓冲区(Ping-Pong Buffer)。但为了简化,我们可以采用“半满中断”或设置一个“软件读指针”。更简单的方法是:主循环定期检查DMA的写入位置(__HAL_DMA_GET_COUNTER可以获取剩余未传输数据量,从而推算已写入量),然后批量读取和处理一段时间内的数据。

  3. 串口通信协议设计 直接发送原始二进制数据不利于上位机解析和纠错。需要设计一个简单的应用层帧协议

    • 帧格式:帧头(2字节,如0xAA,0x55) + 数据长度(1字节) + 命令/数据类型(1字节) + 数据载荷(N字节) + 校验和(1字节,如累加和取反)。
    • 作用:帧头用于同步,长度字段确保帧完整性,校验和用于检错,命令字区分不同类型数据(如ADC数据、传感器状态)。
    // 示例:封装一帧ADC数据并通过UART发送
    void UART_Send_ADC_Frame(uint16_t *data, uint8_t count) {
        uint8_t tx_buffer[256];
        uint8_t index = 0;
        uint8_t checksum = 0;
    
        // 帧头
        tx_buffer[index++] = 0xAA;
        tx_buffer[index++] = 0x55;
    
        // 数据长度 (count个数据,每个2字节,所以总字节数是 count*2)
        uint8_t data_length = count * 2;
        tx_buffer[index++] = data_length;
        checksum += data_length;
    
        // 命令字 (例如0x01代表ADC数据)
        uint8_t cmd = 0x01;
        tx_buffer[index++] = cmd;
        checksum += cmd;
    
        // 数据载荷
        for(int i=0; i<count; i++) {
            tx_buffer[index++] = (data[i] >> 8) & 0xFF; // 高字节
            tx_buffer[index++] = data[i] & 0xFF;        // 低字节
            checksum += tx_buffer[index-2];
            checksum += tx_buffer[index-1];
        }
    
        // 校验和
        tx_buffer[index++] = ~checksum;
    
        // 通过HAL_UART_Transmit发送 tx_buffer
        HAL_UART_Transmit(&huart1, tx_buffer, index, 1000);
    }
    

数据采集与传输示意图

4. 关键工程考量

实现功能只是第一步,让系统稳定可靠运行才是工程师的价值所在。

  1. 实时性:本系统的实时性要求主要体现在“采样率稳定”和“数据不丢失”。使用DMA+定时器触发ADC可以保证精确的采样间隔。确保UART发送速度大于数据产生速度,否则需增加缓冲区或降低采样率。
  2. 噪声抑制
    • 硬件:在传感器信号进入ADC前,使用RC低通滤波电路;ADC基准电压引脚加去耦电容(通常一个10uF钽电容并联一个0.1uF陶瓷电容);模拟和数字地单点连接。
    • 软件:在ADC采样后,可以采用滑动平均滤波中值滤波等简单算法,有效抑制脉冲噪声和平滑数据。
  3. 电源稳定性:模拟部分(ADC、传感器)的供电最好使用LDO(低压差线性稳压器),并与数字部分(MCU、逻辑电路)通过磁珠或0Ω电阻隔离。务必在靠近各芯片电源引脚处放置足够和合适的去耦电容。

5. 生产环境避坑指南

这些经验往往在书本上学不到,却决定了项目的成败。

  1. PCB布局布线
    • 模拟与数字分区:将PCB板按功能分区,模拟部分(传感器接口、ADC、基准源)和数字部分(MCU、晶振、数字接口)尽量分开。
    • 电源路径:电源线应尽量粗短,先经过滤波电容再到达芯片。
    • 阻抗匹配:对于高频信号线(如SPI时钟线、USB差分线),需考虑阻抗控制,保持走线等长、避免锐角。
  2. 固件更新与维护
    • 在项目初期就预留Bootloader串口/IAP升级功能,这将极大方便后期修复bug和升级功能。
    • 代码版本管理使用Git,每次功能实现或修复都做好提交记录。
  3. 调试日志系统
    • 不要只会用printf。建立一个灵活的日志模块,可以通过宏定义控制日志级别(DEBUG, INFO, ERROR),并重定向到不同的输出(串口、LCD、文件系统)。
    • 在关键函数入口、出口和错误处理分支添加日志,这是定位复杂问题的利器。
    // 简单的日志宏定义示例
    #define LOG_LEVEL_DEBUG 0
    #define LOG_LEVEL_INFO  1
    #define LOG_LEVEL_ERROR 2
    
    #define CURRENT_LOG_LEVEL LOG_LEVEL_DEBUG
    
    #define LOG_DEBUG(fmt, ...) do { \
        if(CURRENT_LOG_LEVEL <= LOG_LEVEL_DEBUG) \
            printf("[DEBUG] " fmt "\r\n", ##__VA_ARGS__); \
    } while(0)
    
    #define LOG_ERROR(fmt, ...) do { \
        if(CURRENT_LOG_LEVEL <= LOG_LEVEL_ERROR) \
            printf("[ERROR] %s:%d: " fmt "\r\n", __FILE__, __LINE__, ##__VA_ARGS__); \
    } while(0)
    // 在代码中使用
    LOG_DEBUG("ADC DMA Initialized. Buffer addr: %p", adc_buffer);
    if(HAL_ADC_Init(&hadc1) != HAL_OK) {
        LOG_ERROR("ADC Init Failed!");
    }
    

结语与展望

通过这个“嵌入式信号采集系统”项目,我们完整地走了一遍需求分析、硬件选型、核心驱动开发、通信协议设计、系统调试与优化的流程。这不仅仅是一个毕设,更是一个经典的嵌入式应用框架。

下一步,你可以尝试:

  • 功能扩展:接入更多的数字传感器(I2C/SPI),如陀螺仪、光强度传感器。
  • 性能提升:为系统加入FreeRTOS,将数据采集、处理和通信任务放在不同线程中,提高系统响应能力和代码结构清晰度。
  • 无线化:将UART有线传输替换为蓝牙(HC-05/ESP32)或LoRa模块,实现无线数据遥测。
  • 数据分析:在上位机(可以用Python的PyQt或Tkinter编写)中加入FFT频谱分析功能,让你采集的时域信号展现出频域特征,这会是毕设论文中的一个亮点。

理论终究需要实践来巩固。建议你立即动手,从点亮一个LED开始,逐步添加ADC采样、DMA、串口通信等功能,亲手搭建这个“最小可行系统”。过程中遇到的每一个错误和解决的每一个问题,都会让你对嵌入式系统的理解更加深刻。祝你毕设顺利,收获满满!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐