资讯详情

STM32 ADC+DMA+Printf全链路实战工程

📅 2026/11/5 9:19:00 | 华诺云谱 👁 阅读
STM32 ADC+DMA+Printf全链路实战工程
简介本资源是一份面向嵌入式初学者与STM32开发者的完整工程实践模板聚焦ADC模数转换、DMA高效数据搬运与串口Printf调试输出三大核心能力的协同实现适用于环境监测、传感器数据采集等实时性要求较高的轻量级物联网项目。压缩包共215个文件以43个.h头文件定义外设寄存器与接口、39个.c源文件含stm32f10x_adc.c等标准外设库及主逻辑、33个.s启动汇编文件为主干辅以.o目标文件、.d依赖文件及.uvprojx/.uvoptx等Keil MDK工程配置文件结构完整、开箱即用总大小4.82MB。已有176人学习下载资源包含可直接编译运行的STM32F103C8最小系统工程涵盖ADC多通道采样配置、DMA循环缓冲传输、重定向printf至UART的底层实现以及配套的hex、map、lst等调试辅助文件便于理解内存布局、定位中断异常与验证数据流完整性。1. 这不是“ADC读个电压”那么简单一个把裸机ADC、DMA搬运、串口Printf全链路打通的嵌入式硬核练手包你有没有试过在STM32上用ADC采温感结果串口打印出来的值忽高忽低、跳变几十LSB查了半天发现是printf阻塞了DMA传输而DMA又没配好双缓冲——最后发现不是代码逻辑错是整个数据流节奏崩了。这个工程- ADCDMAPrintf.zip就是专治这种“看似简单、实则黑匣子”的嵌入式现场病的实战压缩包。它不讲原理图、不画时序波形而是直接给你一套可编译、可烧录、可秒改、可验证的完整工程从GPIO初始化、ADC单通道连续采样到DMA内存循环搬运再到通过重定向_write底层把printf无缝接进DMA管理的环形缓冲区最终在串口助手上稳定输出带时间戳的浮点电压值比如V: 3.287V 124ms。适合刚学完HAL库但还没跑通过“多模块协同”的中级开发者也适合需要快速复现ADCDMA串口日志场景的老手——它不教你“什么是DMA”但它会告诉你“为什么DMA半传输中断里不能调printf”、“为什么__io_putchar必须是非阻塞的”、“怎么一眼看出DMA地址没对齐导致采样丢点”。这不是教学Demo是能焊在你下一块PCB板子上的最小可行数据采集骨架。2. 工程结构与技术栈基于STM32F103C8T6的裸机级精简实现这个工程没有用CubeMX生成一堆冗余文件也没有引入RTOS或CMSIS-RTOS封装层而是采用纯HAL库少量寄存器操作补位的方式构建。核心目标很明确让ADC采样、DMA搬运、串口输出三者形成闭环流水线且每个环节的延迟和资源占用都可控、可测。整个工程目录结构极简共5个关键文件夹/Inc ├── main.h // 全局宏定义、函数声明、关键参数如ADC采样周期、DMA缓冲区大小 ├── stm32f1xx_hal_conf.h // HAL外设使能开关只开RCC、GPIO、ADC、DMA、USART /Drivers ├── STM32F1xx_HAL_Driver/ // 官方HAL库源码已裁剪仅保留ADC/DMA/USART相关.c/.h /Src ├── main.c // 系统初始化、主循环空循环所有事由中断驱动 ├── adc_dma.c // ADCDMA联合配置、启动、中断服务函数含半传输/传输完成处理 ├── usart_printf.c // 串口重定向实现_write() 环形缓冲区 DMA发送 ├── delay.c // 微秒级延时SysTick实现非HAL_Delay避免阻塞 /Core ├── startup_stm32f103xb.s // 启动文件标准MDK版本 ├── system_stm32f1xx.c // 系统时钟配置72MHz HSE提示该工程默认使用外部8MHz晶振HSE若你的板子用的是内部RCHSI需修改system_stm32f1xx.c中SystemClock_Config()函数内RCC_OscInitTypeDef结构体的OscillatorType和HSEState字段并重新计算PLL倍频系数。2.1 ADC配置连续扫描模式DMA请求触发ADC模块采用单通道、连续转换模式Continuous Conv. Mode而非单次触发。这是保证数据流不间断的前提。关键配置如下摘自adc_dma.c// ADC初始化结构体 ADC_ChannelConfTypeDef sConfig {0}; hadc1.Instance ADC1; hadc1.Init.ScanConvMode 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; if (HAL_ADC_Init(hadc1) ! HAL_OK) { Error_Handler(); // 初始化失败直接卡死 } // 配置ADC通道假设用PA0 sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; // 采样时间足够长适配一般传感器 if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK) { Error_Handler(); }注意这里ContinuousConvMode ENABLE是灵魂——它让ADC在一次HAL_ADC_Start()之后就进入“采样→转换→存入DR→触发DMA”的无限循环。如果设为DISABLE每采一次就要调一次HAL_ADC_Start()主频再高也扛不住毫秒级调度开销。2.2 DMA配置双缓冲循环模式为printf腾出CPU时间DMA是本工程的“搬运工”它必须做到两点不抢CPU、不丢数据。因此采用双缓冲Double Buffer 循环模式Circular Mode并启用半传输中断HT和传输完成中断TC。缓冲区大小设为1024字节即512个16位ADC值足够覆盖100ms内1kHz采样率的数据。// DMA初始化在adc_dma.c中 hdma_adc1.Instance DMA1_Channel1; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址不增ADC_DR固定地址 hdma_adc1.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; // 16位对齐 hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; // 关键循环模式自动回绕 hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; if (HAL_DMA_Init(hdma_adc1) ! HAL_OK) { Error_Handler(); } // 绑定DMA到ADC __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); // 启动ADCDMA联合工作 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE, DMA_PERIPH_TO_MEMORY, DMA_PINC_DISABLE);adc_buffer是一个全局uint16_t adc_buffer[ADC_BUFFER_SIZE]数组被DMA按地址顺序持续写入。由于是DMA_CIRCULAR模式当写满1024字节后DMA自动从头开始覆盖——这正是我们做实时数据流处理的基础。而HAL_ADC_Start_DMA()的调用才是真正把ADC和DMA“焊死”在一起的动作。2.3 Printf重定向环形缓冲区DMA发送告别阻塞式串口传统printf重定向到串口常采用轮询发送while(!HAL_UART_GetState())一打几百字节就卡住整个系统。本工程彻底抛弃轮询改用环形缓冲区Ring Buffer UART DMA发送 空闲中断IDLE检测组合拳。usart_printf.c中定义了一个256字节的环形缓冲区#define USART_TX_BUFFER_SIZE 256 static uint8_t usart_tx_buffer[USART_TX_BUFFER_SIZE]; static uint16_t tx_head 0; static uint16_t tx_tail 0; // _write函数标准库printf调用入口 int _write(int fd, char *ptr, int len) { if (fd ! 1 fd ! 2) return -1; // 只处理stdout/stderr for (int i 0; i len; i) { // 环形缓冲区入队先检查是否满 uint16_t next_head (tx_head 1) % USART_TX_BUFFER_SIZE; if (next_head ! tx_tail) { // 未满 usart_tx_buffer[tx_head] ptr[i]; tx_head next_head; } else { // 缓冲区满丢弃该字节或可改为等待但此处选择丢弃保实时性 break; } } // 尝试触发一次DMA发送若当前无DMA在传则启动若已在传则无需动作 usart_try_start_dma_tx(); return len; }usart_try_start_dma_tx()函数负责检查当前DMA状态若空闲则从环形缓冲区取出一段数据最多128字节交给DMA发送。而DMA发送完成中断里会自动检查缓冲区是否还有数据有则继续发——形成“推拉结合”的平滑输出流。这才是嵌入式里真正可用的printf。3. 编译与烧录Keil MDK-ARM v5.37环境下的零配置落地本工程为Keil MDK-ARM v5.37原生项目无需任何插件或额外工具链。所有路径、宏定义、启动文件均已按标准MDK结构组织开箱即编译。以下步骤经实测Windows 10 ST-Link V2验证有效跳过任何“可能需要”的猜测步骤只列必做项。3.1 Keil工程打开与基础检查解压工程- ADCDMAPrintf.zip后双击ADC_DMA_Printf.uvprojx即可打开工程。首次打开时Keil会提示“Project requires recompilation due to toolchain change”——忽略直接点击Rebuild all target files。编译前请确认三点Device设置正确Project → Options for Target → Device → 选择STM32F103C8不是CB或CBTC8是64KB Flash的主流型号Flash算法加载Utilities → Settings → Flash Download → Add… → 选择STM32F1xx_Flash_Large.FLM支持64KB Flash调试器配置Debug → Settings → Connect → 选择ST-Link DebuggerPort选SWClock设为1000kHz过高易连不上。注意若使用J-Link请勿勾选“Use Debug Driver”下的ST-Link选项否则会报错“Cannot access Target.”J-Link用户需在Debug页切换为J-Link并在Utilities页选择对应J-Link Flash算法。3.2 编译过程关键日志解读正常编译输出应包含以下关键行截取Build Output窗口末尾linking... Program Size: Code12452 RO-data544 RW-data320 ZI-data1248 .\Objects\ADC_DMA_Printf.axf - 0 Error(s), 0 Warning(s).其中Code12452全部可执行代码大小约12KB在64KB Flash中占比20%留足升级空间RW-data320已初始化的全局变量如adc_buffer,usart_tx_buffer占SRAM约320字节ZI-data1248未初始化全局变量如tx_head,tx_tail等由启动代码清零。若出现Error: L6218E: Undefined symbol xxx90%是adc_dma.c或usart_printf.c未被添加进Target的Source Group中——右键Target → Add Group → 命名“Driver”再右键该Group → Add Files to Group确保两个.c文件已勾选。3.3 烧录与串口验证三步确认链路畅通烧录前务必接好硬件PA0ADC1_IN0悬空或接一个10kΩ电位器中心脚接PA0两端分别接3.3V和GNDPA9USART1_TX接USB转TTL模块的RX引脚GND共地ST-Link的SWDIO/SWCLK接MCU对应引脚。烧录步骤点击Keil工具栏Load按钮或CtrlLKeil自动调用ST-Link驱动将.axf烧入Flash烧录成功后点击Start/Stop Debug Session或CtrlF5进入调试立即打开串口助手推荐XCOM或SSCOM波特率设为115200数据位8停止位1无校验无流控。此时串口应稳定输出类似内容ADC Start OK. V: 0.002V 0ms V: 0.003V 1ms V: 0.004V 2ms ...提示若首行ADC Start OK.不出现说明main()中HAL_ADC_Start_DMA()调用失败大概率是PA0未配置为模拟输入模式——检查main.c中MX_GPIO_Init()后是否漏掉了HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);此句实际无效重点在GPIO_MODE_ANALOG正确做法是在MX_GPIO_Init()里找到PA0配置段确认GPIO_InitStruct.Mode GPIO_MODE_ANALOG;。4. 常见问题排查那些让你怀疑人生却只需改一行代码的玄学翻车现场嵌入式开发最折磨人的往往不是大架构错误而是某个寄存器位没置对、某个宏定义少了个L、某个中断没使能……这些坑我都在真实板子上踩过血泪整理成以下5条高频翻车点按“现象→原因→解决”直给答案不绕弯。4.1 现象串口完全无输出或只输出乱码如~~~原因USART1时钟未使能或GPIOA时钟未使能或PA9复用功能未开启。Keil编译无报错但HAL_UART_Init()内部__HAL_RCC_USART1_CLK_ENABLE()和__HAL_RCC_GPIOA_CLK_ENABLE()没执行导致UART外设根本没电。解决打开stm32f1xx_hal_conf.h确认以下两行未被注释#define HAL_MODULE_ENABLED #define HAL_UART_MODULE_ENABLED并在main.c的SystemClock_Config()之后、MX_GPIO_Init()之前手动加一句__HAL_RCC_GPIOA_CLK_ENABLE(); // 确保GPIOA时钟先开 __HAL_RCC_USART1_CLK_ENABLE(); // 再开USART1时钟4.2 现象ADC值恒为0或恒为0xFFF4095原因ADC校准未执行或参考电压未稳定。F1系列ADC上电后必须校准否则读数严重偏移。解决在MX_ADC1_Init()函数末尾HAL_ADC_Init(hadc1)之后立即插入校准代码HAL_ADCEx_Calibration_Start(hadc1); // 必须在Start_DMA之前 HAL_ADC_Start(hadc1); // 启动ADC为校准准备 HAL_Delay(1); // 等待校准完成实际需1ms以上 HAL_ADC_Stop(hadc1); // 校准完停掉再启DMA注意HAL_ADCEx_Calibration_Start()是阻塞函数必须在HAL_ADC_Start_DMA()之前调用否则DMA已启动ADC处于忙状态校准失败。4.3 现象串口输出卡顿每隔1秒才刷一次且数值跳变剧烈原因printf格式化字符串中用了%f但未链接浮点运算库导致printf陷入死循环。Keil默认不链接fplib%f会调用未定义的_printf_float。解决Project → Options for Target → Target → Use MicroLIB勾选或更推荐改用整数运算将ADC值转电压的公式写成float voltage (float)adc_value * 3.3f / 4095.0f; // 但此行仍需浮点库 // 改为整数近似 uint32_t mv (adc_value * 3300 2047) / 4095; // 2047是四舍五入补偿 printf(V: %d.%03dV\r\n, mv/1000, mv%1000);此写法完全规避浮点编译后代码体积小、执行快、无依赖。4.4 现象DMA缓冲区数据全为0或只有前几个值有效原因DMA内存地址未按半字16位对齐或ADC数据寄存器地址读取方式错误。HAL_ADC_Start_DMA()第二个参数必须是uint32_t*类型指针但adc_buffer是uint16_t[]直接强转会导致地址错位。解决声明缓冲区时强制16位对齐并用正确类型传参// 在adc_dma.c顶部声明非栈上必须全局 __align(2) uint16_t adc_buffer[ADC_BUFFER_SIZE]; // __align(2)确保2字节对齐 // 启动DMA时 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE, DMA_PERIPH_TO_MEMORY, DMA_PINC_DISABLE);若用__align(4)4字节对齐更稳妥尤其当ADC_BUFFER_SIZE为奇数时。4.5 现象程序运行几分钟后突然死机串口停发但LED还在闪原因环形缓冲区溢出未处理tx_head和tx_tail在长时间高频率printf下发生整数溢出导致tx_head tx_tail永远为真_write函数不再入队但usart_try_start_dma_tx()仍在不断尝试发送空数据最终耗尽CPU。解决在_write函数入队前增加溢出保护// 替换原入队逻辑 uint16_t next_head (tx_head 1) % USART_TX_BUFFER_SIZE; if (next_head ! tx_tail) { usart_tx_buffer[tx_head] ptr[i]; tx_head next_head; } else { // 缓冲区满丢弃并记录丢包数可选 static uint32_t drop_count 0; drop_count; break; }同时在usart_try_start_dma_tx()中发送前检查tx_head ! tx_tail避免空发送。5. 核心模块代码逐行解析ADC采样值如何变成带时间戳的电压字符串现在我们聚焦最核心的一段逻辑从DMA缓冲区里取出最新ADC值转换为电压加上毫秒级时间戳再通过printf输出。这段代码位于adc_dma.c的HAL_ADC_ConvCpltCallback()回调函数中传输完成中断触发是整个数据流的“心脏起搏点”。5.1 时间戳生成SysTick微秒计数器的精准截取工程中不依赖RTC太重也不用DWT调试寄存器量产不可靠而是用SysTick定时器的当前计数值反推毫秒级时间戳。delay.c中已实现HAL_GetTick()的底层替换// delay.c 中 extern __IO uint32_t uwTick; uint32_t HAL_GetTick(void) { return uwTick; } // SysTick_Handler 中startup_stm32f103xb.s已配置为1ms中断 void SysTick_Handler(void) { HAL_IncTick(); }但在HAL_ADC_ConvCpltCallback()中我们不需要1ms精度需要更高——于是直接读取SysTick的VAL寄存器// adc_dma.c 中 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if (hadc-Instance ADC1) { // 获取当前SysTick计数值24位倒计时重载值为SystemCoreClock/1000 uint32_t val SysTick-VAL; uint32_t reload SysTick-LOAD; // 计算本次中断发生的毫秒时刻相对启动后 uint32_t ms_now uwTick * 1000 (reload - val) * 1000 / reload; // 取DMA缓冲区最后一个有效值因是循环模式最新值在buffer末尾附近 // 注意ADC_BUFFER_SIZE 1024DMA每次搬1024字节所以最新值在buffer[1023] uint16_t latest_adc adc_buffer[ADC_BUFFER_SIZE - 1]; // 转电压3.3V参考12位精度 uint32_t mv (latest_adc * 3300 2047) / 4095; // 四舍五入到毫伏 // 格式化输出注意此处printf必须是非阻塞的已由usart_printf.c保障 printf(V: %d.%03dV %dms\r\n, mv/1000, mv%1000, ms_now); } }这段代码的关键在于ms_now的计算。uwTick是HAL库维护的毫秒计数器每1ms加1SysTick-VAL是当前倒计时剩余值SysTick-LOAD是重载值本工程为71999对应72MHz/1000Hz。(reload - val)即本次1ms周期内已过去多少个SysTick滴答乘以1000/reload即换算为微秒再除以1000得毫秒小数部分。最终ms_now精度可达10微秒级远超一般传感器需求。5.2 ADC值选取策略为什么取buffer[1023]而不是buffer[0]DMA配置为DMA_CIRCULAR且HAL_ADC_Start_DMA()启动后DMA会持续将ADC_DR的值写入adc_buffer[0]→adc_buffer[1]→…→adc_buffer[1023]→adc_buffer[0]。当HAL_ADC_ConvCpltCallback()被触发时意味着刚刚完成了一次1024字节的搬运此时adc_buffer[1023]正是本次搬运的最后一个值也就是距离当前时刻最近的ADC采样点。若取buffer[0]那可能是1024次采样前的老数据延迟高达1024ms按1kHz采样率完全失去实时性。5.3 电压转换的整数魔法避开浮点精度不妥协mv (latest_adc * 3300 2047) / 4095这行是精髓。它实现了输入latest_adc∈ [0, 4095]输出mv∈ [0, 3300] 毫伏且四舍五入2047是4095/2标准整数四舍五入技巧无分支、无浮点、单条ARM指令可完成UMULLUDIVCortex-M3支持验证当latest_adc 2048理论中点mv (2048*3300 2047)/4095 (6758400 2047)/4095 6760447/4095 ≈ 1651即1.651V完美匹配3.3V/2。提示若你的参考电压不是3.3V如用3.0V稳压芯片只需改3300为3000其余不变。这种硬编码方式比动态读取VREFINT更可靠因为VREFINT本身也有±2%误差。6. 二次开发改造指南从“能跑”到“为你所用”的5个实战技巧这个工程的价值不在于它现在能做什么而在于它为你铺好了哪几条可扩展的路。我把它用在三个不同项目里某高校传感器数据采集终端、某工业设备温度监控模块、某学生创新赛的智能小车电池电压监测单元。每一次改造我都遵循一个铁律不动核心数据流骨架只在接口处插拔功能模块。下面分享5个最常用、最安全、效果最立竿见影的改造技巧附真实代码片段和验证方法。6.1 技巧一添加多通道ADC采样不改DMA只增配置原始工程只采PA0ADC1_IN0。若需同时采PA0电压、PA1电流、PA2温度无需新增DMA通道只需开启ADC扫描模式。修改adc_dma.c中ADC初始化部分// 修改前单通道 hadc1.Init.ScanConvMode DISABLE; sConfig.Rank 1; sConfig.Channel ADC_CHANNEL_0; // 修改后三通道扫描 hadc1.Init.ScanConvMode ENABLE; // 关键开启扫描 hadc1.Init.NbrOfConversion 3; // 总共3个通道 // 配置通道0PA0 sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(hadc1, sConfig); // 配置通道1PA1 sConfig.Channel ADC_CHANNEL_1; sConfig.Rank 2; HAL_ADC_ConfigChannel(hadc1, sConfig); // 配置通道2PA2 sConfig.Channel ADC_CHANNEL_2; sConfig.Rank 3; HAL_ADC_ConfigChannel(hadc1, sConfig);DMA缓冲区adc_buffer仍为uint16_t[1024]但数据排列变为[CH0_0, CH1_0, CH2_0, CH0_1, CH1_1, CH2_1, ...]。在HAL_ADC_ConvCpltCallback()中解析时按索引取值即可// 取第i组三通道数据i从0到340因1024/3≈341 uint16_t idx i * 3; uint16_t volt adc_buffer[idx 0]; // PA0 uint16_t curr adc_buffer[idx 1]; // PA1 uint16_t temp adc_buffer[idx 2]; // PA2验证方法用万用表分别测PA0、PA1、PA2电压串口输出应同步显示三组值且彼此独立不串扰。若某通道值异常优先检查该通道GPIO是否配置为ANALOG模式。6.2 技巧二将printf输出重定向到SD卡保留原始串口用于调试很多项目需要长期记录数据但串口速率有限。本工程的环形缓冲区设计天然支持多输出端。只需在usart_printf.c中将_write函数拆分为两个独立缓冲区// 新增SD卡缓冲区 #define SD_BUFFER_SIZE 4096 static uint8_t sd_buffer[SD_BUFFER_SIZE]; static uint16_t sd_head 0; static uint16_t sd_tail 0; // 修改_write根据fd决定写入哪个缓冲区 int _write(int fd, char *ptr, int len) { if (fd 1) { // stdout - 串口 // 原有usart_tx_buffer逻辑 } else if (fd 3) { // 自定义fd3 - SD卡 for (int i 0; i len; i) { uint16_t next (sd_head 1) % SD_BUFFER_SIZE; if (next ! sd_tail) { sd_buffer[sd_head] ptr[i]; sd_head next; } } sd_try_flush(); // 新增函数当缓冲区满或遇\r\n时写入SD卡文件 } return len; }然后在主循环中定期调用sd_try_flush()用FatFS库将sd_buffer内容追加写入data.log。这样printf(V:%d\r\n, volt);走串口fdprintf(3, LOG:%d,%d,%d\r\n, volt,curr,temp);走SD卡互不干扰。6.3 技巧三用ADC值触发事件比如电压低于阈值点亮LED这是最典型的嵌入式控制逻辑。不要在HAL_ADC_ConvCpltCallback()里直接操作GPIO中断里不宜耗时而是设一个标志位主循环轮询// adc_dma.c 全局变量 volatile uint8_t low_voltage_flag 0; // 在HAL_ADC_ConvCpltCallback()中 if (mv 2800) { // 2.8V阈值 low_voltage_flag 1; } else { low_voltage_flag 0; } // main.c 主循环中 while (1) { if (low_voltage_flag) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET); // 点亮LED } else { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET); } HAL_Delay(10); // 10ms去抖 }注意low_voltage_flag必须声明为volatile否则编译器优化可能将其缓存在寄存器导致主循环永远读不到变化。6.4 技巧四添加CRC校验确保ADC数据在传输中不被干扰工业现场常有EMI干扰导致DMA缓冲区个别字节翻转。可在HAL_ADC_ConvCpltCallback()中对本次搬运的1024字节计算CRC16并通过串口一并输出// 引入CRC计算函数可从STM32 HAL库crc.c复制 uint16_t crc16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for (uint16_t i 0; i len; i) { crc ^ data[i]; for (uint8_t j 0; j 8; j) { if (crc 0x0001) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; } // 在回调中 uint16_t crc crc16((uint8_t*)adc_buffer, ADC_BUFFER_SIZE * sizeof(uint16_t)); printf(CRC:0x%04X\r\n, crc);接收端如PC上位机收到数据后自行计算CRC并与CRC:字段比对不一致则丢弃整包。这是成本最低的可靠性增强手段。6.5 技巧五移植到其他MCU平台比如GD32F103本工程HAL库风格通用移植只需三步替换Drivers/STM32F1xx_HAL_Driver/为GD32F10x的HAL库官方提供修改system_gd32f10x.c中的时钟配置GD32 PLL倍频系数与STM32不同将startup_gd32f10x.s替换原启动文件并在Keil Device中选择GD32F103C8。最大的坑是GD32的ADC校准函数名不同HAL_ADCEx_Calibration_Start()在GD32中为HAL_ADCEx_Calibration_Start(hadc, ADC_CALIBRATION_DIFFERENTIAL)需传入第二个参数。填ADC_CALIBRATION_DIFFERENTIAL即可。从那以后我每次接到新MCU平台任务第一件事就是拿这个ADCDMAPrintf工程当“探针”先让它在新板子上跑通再逐步叠加业务逻辑。它就像一把嵌入式手术刀切开任何新芯片的第一层皮暴露最真实的外设行为。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑