ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32调幅信号处理实验电路源码解析:从ADC采样到CAN上报

2026/9/11 20:41:14 拓冰建站 浏览量
STM32调幅信号处理实验电路源码解析:从ADC采样到CAN上报 简介2017年全国大学生电子设计竞赛F题“调幅信号处理实验电路”完整方案与源码包面向电子信息类本科生与电赛备赛者可帮助理解调幅信号产生、处理与检测的工程实现路径课程设计或毕业设计亦可参考。压缩包共110个文件以C语言源码和Keil工程文件为主其中43个.c与43个.h文件构成模块化代码结构从底层驱动到应用层显示分层清晰配套uv2、uvprojx、uvoptx工程配置、hex固件、PDF文档及串行12864/液晶驱动便于直接编译烧录验证整包仅1.16MB。程序均经过测试可直接运行覆盖信号调理、ADC采集、定时器控制及显示输出完整链路预览中的标准外设库还涉及I2C、CAN等驱动可学习STM32的库函数调用、外设初始化流程与信号处理思路。已有394人学习下载对智能仪表与信号处理类赛题有较高参考价值。1. 调幅信号处理实验电路从一个F题到完整STM32工程2017年大学生电赛F题“调幅信号处理实验电路”让很多队伍栽在“显示”和“通信”上。模拟前端用包络检波和运放搭好之后调制度能不能实时显示、数据能不能送给另一块板全靠MCU侧的工程代码撑起来。这份源码正好把STM32F10x的ADC、定时器、LCD、I2C、CAN全部串在了一起构成一个可以直接运行的调幅信号处理链路。对于正在备赛的本科生来说它最直接的帮助是省去外设初始化一遍遍查库函数的时间对于工作五年以上想快速捡起标准外设库的人这份工程也是理解“定时器触发采样 外部总线回传”的好样例。它不追求高端算法贵在完整、简单、可复现。2. 源码结构与外设分配读透stm32f10x系列文件2.1 认识工程文件从文件名判断外设职责解压这个F题源码包后第一眼看到的就是一堆stm32f10x_*.c。这里面既有标准外设库的驱动也有工程自己的封装。STARTUP.A51是汇编启动文件lcd.c显然是自己写的显示驱动keilkilll.bat是清理脚本其余以stm32f10x_开头的文件大多是ST官方标准外设库源码。把这些文件名与调幅信号处理的任务对应起来就能明白硬件设计者当时把外设分配到了哪些地方。下表逐一列出了可见文件的角色定位。文件在调幅信号处理链路中的角色STARTUP.A51定义堆栈与中断向量把执行流引导到__mainstm32f10x_rcc.c复位与时钟控制配置72MHz主频及外设总线分频stm32f10x_tim.c定时器初始化负责生成采样脉冲和载波相关信号stm32f10x_adc.cADC外设驱动完成AM包络电压到数值的转换stm32f10x_flash.c内部Flash驱动用于固化校准参数stm32f10x_i2c.cI2C外设驱动连接EEPROM保存掉电数据stm32f10x_can.cCAN外设驱动把调制度结果发送到总线上lcd.cLCD屏驱动显示调制度、最大值、最小值等参数从这串文件就能猜出来这个工程不是简单地采一个电压而是把测量结果显示出来并且支持参数存储和总线上报。尤其注意stm32f10x_can.c的存在说明这套系统在有多个设备协作的赛题场景里预留了数据出口。2.2 启动文件与时钟配置2.2.1 STARTUP.A51为什么不能乱换STARTUP.A51在STM32工程里实际上起着GCC启动文件的作用。上电后CPU从0x00000000取出栈顶指针再从中断向量表取复位异常入口执行的是这段汇编代码。它初始化堆栈指针SP拷贝RW数据到RAM清零ZI段最后调用__main。如果你把一颗64KB Flash的C8工程烧到128KB的RC芯片启动文件仍能运行但stm32f10x_flash.c里的扇区地址会出错所以检查芯片型号后要同步检查启动文件。2.2.2 RCC所有外设时钟的前提stm32f10x_rcc.c里封装了时钟使能函数但真正决定主频的是main里对RCC_PLLConfig的调用。下面是电赛板子上最常用的72MHz配置写法void SystemClock_Config(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); while (RCC_WaitForHSEStartUp() ! SUCCESS); RCC_PLLConfig(RCC_PLLSource_PREDIV1, RCC_PLLMul_9); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); RCC_HCLKConfig(RCC_SYSCLK_Div1); RCC_PCLK2Config(RCC_HCLK_Div1); RCC_PCLK1Config(RCC_HCLK_Div2); }说明外部晶振8MHzPLL倍频9倍得到72MHz系统时钟。APB1分频器设为2所以定时器、I2C、CAN这些APB1外设输入时钟是36MHz这个数值会影响后面的CAN分频和定时器预分频。APB2分频1得到72MHzADC和GPIO都在这条总线上采样时钟需要再单独分频到不超过14MHz。2.3 第一步编译让源码在Keil里跑起来拿到工程不要急着改代码先做一次Clean再重新编译。keilkilll.bat脚本正是用来清理中间文件的双击执行后它会把.o、.crf、.axf、.hex等过程文件删除。如果工程是在别处拷来的中间文件里残留着别人的绝对路径会导致Keil找不到中间产品。清理后再打开.uvprojx全量编译一次烧录到STM32F103开发板。若LCD屏有显示并且CAN没有异常说明时钟和外设配置基本正确。提示如果下载后程序跑飞先检查STARTUP.A51与芯片容量的匹配再检查RCC_PLLConfig里的倍频数是否让内部时钟超过72MHz上限。3. ADC与TIM协同AM信号包的采集与解算3.1 为什么用定时器触发ADC而不是软件死等AM解调出来的包络是连续变化电压采样时刻不稳定会直接导致显示值来回跳。最可靠的方案是让定时器更新事件触发ADC采样这样采样点均匀等间隔。定时器触发ADC的硬件链路是TIMx更新事件通过TRGO内部信号连接到ADC外部触发输入。在标准外设库中设置TIM_SelectOutputTrigger(TIM3, TIM_TRGOSource_Update)后再把ADC_ExternalTrigConv设为ADC_ExternalTrigConv_T3_TRGO即可。常见的做法是采样率取1kHz到2kHz。1kHz适合语音信号包络2kHz适合频率更高的模拟方波包络。过采样并没有意义因为包络检波电路本身已经把载波滤掉了采样率只要高于恢复信号的带宽即可。3.2 ADC初始化参数表与示例代码下表是我在验证这个F题源码时建议检查的关键参数参数推荐值说明ADC_SampleTime239.5周期保证电压采样稳定减小源阻抗影响ADC_ContinuousConvModeDISABLE外部触发方式下必须关闭连续转换ADC_ExternalTrigConvT3_TRGO定时器3触发ADC启动ADC_DataAlign右对齐12位结果直接存u16变量ADC_NbrOfChannel1只采集包络电压通道ADC_CLKPCLK2/612MHz满足ADC最大采样频率初始化代码结合标准外设库void ADC1_Init_With_TIM3(void) { GPIO_InitTypeDef gpioInit; ADC_InitTypeDef adcInit; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_ADCCLKConfig(RCC_PCLK2_Div6); gpioInit.GPIO_Pin GPIO_Pin_1; gpioInit.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, gpioInit); adcInit.ADC_Mode ADC_Mode_Independent; adcInit.ADC_ScanConvMode DISABLE; adcInit.ADC_ContinuousConvMode DISABLE; adcInit.ADC_ExternalTrigConv ADC_ExternalTrigConv_T3_TRGO; adcInit.ADC_DataAlign ADC_DataAlign_Right; adcInit.ADC_NbrOfChannel 1; ADC_Init(ADC1, adcInit); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5); ADC_ExternalTrigConvCmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }参数说明RCC_ADCCLKConfig(RCC_PCLK2_Div6)把72MHz的APB2时钟除以6得到12MHz ADC时钟低于14MHz上限。ADC_RegularChannelConfig里的1表示规则组排序位置ADC_Channel_1表示PA1引脚第二功能是ADC通道1如果硬件板上包络信号接在PA0就改成ADC_Channel_0。最后两块校准代码是每次上电都必须做的否则ADC结果会存在偏移误差调制度计算也随之不准。这里依赖TIM3的初始化前文已说明使用1kHz采样频率时TIM_Period 999、TIM_Prescaler 35。3.3 采到的包络如何换算调制度定时器触发后ADC结果可以放在DMA循环缓冲区也可以在中断里读取。最简单的方式是在外部触发中断里读但为了不影响采样时刻我一般用DMA把数据搬到数组。下面是从50个采样点计算调制度的函数适合放在主循环里调用uint16_t adc_buf[50]; float vmax, vmin; float AM_CalModulation(void) { vmax 0; vmin 4095; for (uint8_t i 0; i 50; i) { if (adc_buf[i] vmax) vmax adc_buf[i]; if (adc_buf[i] vmin) vmin adc_buf[i]; } return (vmax - vmin) / (vmax vmin); }这里adc_buf在DMA完成一半或全部时更新主循环直接读。调制度m的范围是0到1当m超过1说明包络已经削底过深实际电路出现过调幅失真。然后把这个m值送到LCD显示同时通过CAN总线发送出去。提示如果采集到的序列最大值和最小值非常接近先别怀疑公式去看包络检波电路是否把电容放电电阻选得太大了。4. LCD / I2C / CAN显示、存储与数据上行的落点4.1 LCD驱动把调制度变成看得见的数据液晶显示是F题评测中被打分的关键lcd.c里通常会封装LCD_Init(),LCD_Clear(),LCD_ShowChar()这类接口。以最简单的LCD1602为例初始化时先把数据口和RS、RW、E脚配置为推挽输出然后发送功能设置指令。常见的时间序写法是void LCD_Init(void) { GPIO_InitTypeDef gpioInit; gpioInit.GPIO_Pin LCD_DB0 | LCD_DB1 | LCD_DB2 | LCD_DB3 | LCD_DB4 | LCD_DB5 | LCD_DB6 | LCD_DB7; gpioInit.GPIO_Mode GPIO_Mode_Out_PP; gpioInit.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(LCD_DATA_PORT, gpioInit); gpioInit.GPIO_Pin LCD_RS | LCD_RW | LCD_E; GPIO_Init(LCD_CTRL_PORT, gpioInit); LCD_SendCmd(0x38); LCD_SendCmd(0x0C); LCD_SendCmd(0x06); }逻辑说明0x38设定为8位数据接口、双行显示、5x7点阵字体0x0C命令关闭光标闪烁0x06让写字符后地址自动加1。每次显示一个字符前需要先等待液晶忙标志清零否则高速单片机会跑到液晶模块内部时序跟不上而丢字。实际测试中如果字符乱码多半是HD44780兼容指令时序设置不对或相位错。4.2 I2C存储偏置校准值掉电不丢AM包络信号经过运放会叠加直流偏置不同电路板之间这个偏置还有差异。现场测试时用精密电位器校准后把偏置值存到外部EEPROM下次上电直接读取省去每次重新调。stm32f10x_i2c.c就是对STM32硬件I2C的配置。硬件I2C需要开启时钟、设定时序和使能中断。写单个字节的核心流程如下static void I2C_EE_WriteByte(uint16_t addr, uint8_t data) { I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, 0xA0, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C1, (uint8_t)(addr 8)); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); I2C_SendData(I2C1, (uint8_t)(addr 0xFF)); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); I2C_SendData(I2C1, data); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)); I2C_GenerateSTOP(I2C1, ENABLE); }参数说明0xA0是EEPROM器件地址右移一位前的写地址也就是A0引脚接地时的固定地址。每次等待事件都要加超时计数因为I2C器件在写内部Flash周期内不响应ACK没有超时会导致程序卡死。这里的EEPROM常见型号是AT24C02最多256字节存放调制度阈值和偏置值足够。4.3 CAN总线把测量结果发出去在电赛组合系统中F题电路经常需要把测得的调制度发送给主控显示或者让双机互传数据。CAN外设配置项比较多先看在250kbps波特率下的几个关键参数CAN参数数值来源CAN_Prescaler936MHz / (9*16) 250kbpsCAN_BS18tq位时间段1CAN_BS27tq位时间段2CAN_SJW1tq同步跳转宽度对应的初始化代码void CAN1_Init(void) { CAN_InitTypeDef canInit; GPIO_InitTypeDef gpioInit; RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpioInit.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_9; gpioInit.GPIO_Mode GPIO_Mode_AF_PP; gpioInit.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpioInit); canInit.CAN_Prescaler 9; canInit.CAN_Mode CAN_Mode_Normal; canInit.CAN_SJW CAN_SJW_1tq; canInit.CAN_BS1 CAN_BS1_8tq; canInit.CAN_BS2 CAN_BS2_7tq; canInit.CAN_TTCM DISABLE; canInit.CAN_ABOM ENABLE; canInit.CAN_AWUM DISABLE; canInit.CAN_NART DISABLE; canInit.CAN_RFLM DISABLE; canInit.CAN_TXFP ENABLE; CAN_Init(CAN1, canInit); }CAN发送调制度时CanTxMsg的Data[0]可以存放(uint8_t)(am_index * 100)Data[1]存放最大值低字节Data[2]存放最小值低字节。发送前检查CAN_TransmitStatus(CAN1, 0)是否返回CAN_TxStatus_Failed失败时重试或丢弃避免阻塞主循环。注意CAN波特率设置错了不会报编译错误只会让总线上所有节点都收不到数据排查时优先用CAN分析仪看波形位宽。4.4 外设并行的优先级问题这个工程的三个外设都在主循环和中断之间并行工作调度顺序稍有不当就会互相干扰。我的建议是ADC定期采样由DMA搬运DMA传输完成中断优先级设为最高CAN发送用于低频率上报优先级次之LCD刷新放在主循环不做任何中断。这样调制度计算不会因为CAN重试而产生锯齿。5. 用keilkilll与调试器校准AM解调链路5.1 keilkilll.bat的隐藏作用keilkilll.bat在比赛前的作用远不只是清理磁盘空间。大赛评测现场机器很乱如果工程带着.uvguix.Administrator这类用户配置或中间.o文件打开工程可能因为绝对路径不对而反复失败。先运行这个批处理再重新打开工程就是干净状态。典型内容如下echo off del /s /q *.o *.d *.crf *.axf *.hex *.lst *.htm *.map del /s /q object\*.* del /s /q listings\*.* echo Done.注意这个脚本删除了.axf文件所以清理之后第一次烧录前必须重新编译。在提交作品时保留keilkilll.bat能让评委快速复现环境这也是很多拿奖工程包里的标配。5.2 用TRGO引脚验证采样时刻验证ADC采样是否准确不需要写复杂逻辑直接把TIM3的TRGO映射到引脚上示波器测量。部分微控制器没有直接引出TRGO可以在TIM3中断里翻转空闲引脚但不建议这么做因为中断进出会引入几微秒误差。如果资源允许用定时器比较输出通道产生PWM然后把PWM的更新事件映射到ADC更接近真实链路。5.3 内存中定位残留问题当LCD显示数字在小数点后一位与后两位之间跳变时通常不是计算问题而是浮点转换字符串时缓冲区溢出或格式化字符串写到了数组边界外。把调制度乘以100转成int再查表显示避开%.2f这类浮点格式化能省去不少调试时间。最后用调试器在Flash校准参数写入处打一个变量监测点检查写入前的CRC校验值基本就能把掉电丢失问题钉死。提示比赛前最后一晚不要更换开发环境统一用Keil MDK5和ST-Link其他工具链转出来的工程在评测机器上很容易因为缺少设备文件而编译失败。本文还有配套的精品资源点击获取