HNU-嵌入式系统-实战演练:基于STM32的AD、DMA与串口通信综合应用
1. 项目背景与核心功能第一次接触STM32开发板时你可能觉得它就是个带芯片的小板子。但当我真正把ADC、DMA和串口通信这三个功能结合起来做项目时才发现它的强大之处。这个实战项目最有趣的地方在于它模拟了一个真实的智能硬件控制场景——就像你家里的智能台灯既能通过按键调节亮度又能用手机APP远程控制。整个系统有两大工作模式流水灯模式和数码管显示模式。在流水灯模式下8个LED灯会像跑马灯一样流动而且你还能通过电脑串口指令控制灯的流动方向和速度。切换到数码管模式后开发板上的4位数码管可以显示0-9的数字通过导航键就能实现数字加减。最妙的是Key3键它就像个模式切换开关按一下就能在两个模式间来回切换。2. 硬件平台搭建2.1 开发板选型与基础配置我用的是STM32F103C8T6核心板这块蓝色的小板子性价比超高淘宝上30块钱就能买到。在CubeMX里新建工程时芯片型号要选对时钟树配置建议直接使用默认的外部8MHz晶振经过PLL倍频到72MHz主频。GPIO配置有个小技巧LED灯接的是PC0-PC7数码管段选接PA0-PA7位选接PB0-PB2这些在原理图上都要提前标好。2.2 外设接口分配ADC按键的配置要特别注意导航键接的是PC0ADC1_IN10Key1和Key2接的是PA0和PA1配置为外部中断模式。串口1USART1用来和电脑通信TX接PA9RX接PA10。我在第一次接线时犯了个错误把TX和RX接反了结果串口助手上一片空白调试了半天才发现问题。3. ADC按键处理实战3.1 ADC采样原理与配置ADC模块就像个电子秤把电压值转换成数字信号。导航键本质上是五个电阻分压按钮每个按键按下时会产生不同的电压值。在CubeMX里配置ADC1时要开启连续转换模式和中断使能采样周期设为55.5个时钟周期比较合适。这里有个坑如果采样时间太短读取的值会不稳定。// ADC初始化关键代码 hadc1.Instance ADC1; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1;3.2 按键去抖与状态识别直接读取ADC值会碰到抖动问题就像老式收音机的旋钮接触不良时数值会跳变。我的解决方案是连续采样5次取中间3个值的平均数。通过实验测得各按键的典型值范围Key1: 0-100Key2: 1000-1500Key3: 2500-3000上拨: 3500-4000下拨: 4000-4095// 按键识别代码示例 uint8_t getNavigationKey(uint16_t adcVal) { if(adcVal 100) return KEY1; else if(adcVal 1500) return KEY2; else if(adcVal 3000) return KEY3; else if(adcVal 4000) return NAV_UP; else return NAV_DOWN; }4. DMA串口通信优化4.1 三种通信方式对比刚开始我用的是阻塞式串口发送结果发现每次发送数据时CPU就像被点了穴一样卡住不动。后来改成中断方式虽然不卡了但发送长字符串时频繁进中断系统效率还是低。最终选择DMA方案后CPU彻底解放了就像请了个专职快递员数据搬运完全不用操心。方式CPU占用率适用场景阻塞式100%调试简单命令中断式30%-50%中等数据量传输DMA5%大数据量实时传输4.2 CubeMX配置技巧在CubeMX配置USART1时要特别注意DMA通道的选择。TX用DMA1 Channel4RX用DMA1 Channel5。有个容易忽略的设置在DMA配置页面要把Memory和Peripheral都设为增量模式Increment否则只能发送/接收第一个字节。开启串口全局中断后记得在代码里实现HAL_UARTEx_RxEventCallback回调函数。// DMA串口接收初始化 HAL_UARTEx_ReceiveToIdle_DMA(huart1, rxBuffer, RX_BUFFER_SIZE); __HAL_DMA_DISABLE_IT(hdma_usart1_rx, DMA_IT_HT);5. 多任务协同设计5.1 定时器中断管理系统用了两个定时器TIM2负责1ms时基TIM3负责20ms时基。在TIM2中断里处理数码管动态扫描TIM3中断处理流水灯效果。这里有个重要经验中断服务函数一定要短小精悍我曾经在里面做了复杂的数学运算结果导致系统卡顿。正确的做法是设置标志位在主循环里处理实际逻辑。// 定时器中断示例 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim htim2) { // 1ms定时器 if(mode DIGITAL_TUBE) digitalTubeScan(); } else if(htim htim3) { // 20ms定时器 if(mode WATER_LIGHT) waterLightFlow(); } }5.2 状态机实现模式切换系统状态用枚举变量管理比用宏定义更直观。我设计的状态转换逻辑如下默认启动为流水灯模式按下Key3切换模式模式切换时要完成资源释放和初始化串口指令只在流水灯模式下有效typedef enum { WATER_LIGHT, DIGITAL_TUBE } SystemMode; SystemMode mode WATER_LIGHT; void switchMode() { if(mode WATER_LIGHT) { mode DIGITAL_TUBE; HAL_UART_Transmit(huart1, Switch to Digital Tube Mode\r\n, 30, 100); } else { mode WATER_LIGHT; HAL_UART_Transmit(huart1, Switch to Water Light Mode\r\n, 29, 100); } }6. 调试经验与性能优化6.1 常见问题排查第一次调试DMA串口时遇到数据接收不全的问题。后来发现是DMA缓冲区太小而且没有处理半传输中断。解决方法有两个要么增大缓冲区要么像我一样直接禁用半传输中断。ADC采样不稳定也是个常见问题可以通过软件滤波解决我用的移动平均法效果就不错。6.2 资源占用分析使用STM32CubeMonitor查看资源占用情况特别有用。实测发现纯中断方式下CPU占用率约45%启用DMA后降到8%以下内存占用最大的是串口接收缓冲区设为256字节比较合适栈空间要预留足够我设置到1.5KB才稳定7. 功能扩展思路这个基础框架其实能玩出很多花样。比如加入PWM调光功能让LED亮度可调实现串口协议解析支持JSON格式指令添加温湿度传感器在数码管上显示环境数据用FreeRTOS改造为多任务系统我在最新版本中增加了加速度计功能倾斜开发板就能控制流水灯方向这个改动只用了不到50行代码。这说明好的架构设计确实能让功能扩展事半功倍。