
简介本资源是一套基于STM32微控制器实现LCD实时波形绘制的完整嵌入式开发工程面向嵌入式初学者与硬件开发者解决数据采集后本地可视化显示的核心需求适用于智能仪表、传感器监测、教学实验等场景。压缩包共136个文件以35个C源文件如lcd.c、stm32f10x_adc.c和33个头文件h为主体涵盖LCD驱动、ADC采样、定时器控制及图形绘制等核心模块另有编译中间文件o/d、Keil工程配置uvprojx、uvoptx、sct、可执行镜像hex/axf及调试脚本bat结构规范便于理解底层时序与工程组织逻辑。已有4529人学习下载。读者可直接导入Keil MDK运行获得从GPIO初始化、LCD多级指令配置、RGB565像素映射、ADC动态采样到逐点/逐行波形刷新的全链路代码实现并支持DMA加速传输与面向对象式LCD类封装具备良好可拓展性是深入掌握STM32外设协同与嵌入式图形开发的优质实践范例。1. 这不是示波器但能让你在STM32上“看见”信号实时波形绘制不是炫技而是调试刚需某开发者在调试电机驱动板时发现电流采样值忽高忽低用万用表只能看平均值用逻辑分析仪又抓不到连续变化趋势——他临时把ADC读数通过串口发到PC上用Python画图结果发现波形毛刺集中在PWM切换瞬间。等他改完代码再测问题已消失。这种“滞后式可视化”在嵌入式现场就是慢性失血。而这个基于STM32的LCD实时绘制波形图项目直接把120×64点阵LCD或兼容ST7735/ILI9341的彩屏变成你的口袋示波器ADC采样→环形缓冲区→坐标映射→逐点刷新全链路在裸机环境下跑通无RTOS、无GUI库、无外部依赖。它不追求百万采样率但保证每毫秒更新一次画面且CPU占用率压在15%以内适合做电源纹波观测、传感器动态响应验证、音频包络跟踪等真实场景。如果你正卡在“数据有但看不见变化”或者想给毕业设计加一个能现场演示的视觉反馈模块这个项目就是你该立刻烧录进开发板的那块代码。2. 硬件与驱动层从GPIO初始化到LCD像素级控制2.1 STM32硬件资源分配与引脚映射本项目基于STM32F103C8T6“蓝 pill”最小系统板实现但结构可平移至F4/F7系列。关键外设分配如下ADC1使用通道0PA0采集模拟信号配置为连续扫描模式采样周期144个ADC时钟周期对应约1μs单次采样触发源为软件触发便于精确控制采样时机TIM2作为主定时器设定为1ms中断周期ARR999PSC7199假设系统时钟72MHz用于驱动波形刷新节奏SPI1驱动LCD以ST7735为例SCK→PA5MOSI→PA7CS→PA4DC→PA3RST→PA2LED可选PB1用于指示ADC采样状态每100次采样闪烁一次辅助肉眼判断数据流是否正常。提示若使用ILI9341等并行接口LCD请将DC/RST/CS引脚改为普通GPIO输出并在lcd_init()中替换初始化序列。本项目默认SPI模式因引脚复用少、布线简洁更适合新手快速验证。2.2 LCD底层驱动避开寄存器黑洞的实用封装ST7735的初始化不是简单写几个寄存器而是存在严格时序依赖如睡眠退出后需等待120ms。本项目将关键操作封装为原子函数避免初学者陷入“为什么屏幕不亮”的玄学排查// lcd_driver.c void lcd_write_cmd(uint8_t cmd) { HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_RESET); // DC0 for command HAL_SPI_Transmit(hspi1, cmd, 1, HAL_MAX_DELAY); } void lcd_write_data(uint8_t data) { HAL_GPIO_WritePin(LCD_DC_GPIO_Port, LCD_DC_Pin, GPIO_PIN_SET); // DC1 for data HAL_SPI_Transmit(hspi1, data, 1, HAL_MAX_DELAY); } void lcd_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { // 设置GRAM写入窗口避免全屏刷导致卡顿 lcd_write_cmd(0x2A); // Column Address Set lcd_write_data(x0 8); lcd_write_data(x0 0xFF); lcd_write_data(x1 8); lcd_write_data(x1 0xFF); lcd_write_cmd(0x2B); // Page Address Set lcd_write_data(y0 8); lcd_write_data(y0 0xFF); lcd_write_data(y1 8); lcd_write_data(y1 0xFF); lcd_write_cmd(0x2C); // Memory Write }参数说明x0/y0窗口左上角坐标0≤x0x1≤1200≤y0y1≤160x1/y1窗口右下角坐标注意ST7735实际分辨率为128×160但常用区域为120×160以避让边框lcd_set_window()是性能关键——每次只刷新波形区域如x0~119, y50~150而非整屏128×16020480像素实测刷新耗时从86ms降至12ms。2.3 ADC采样与环形缓冲区用裸机实现“零拷贝”数据流波形绘制的核心矛盾是ADC采样速率本项目设为1kHz远高于LCD刷新率100Hz必须缓存中间数据。本项目采用128深度的uint16_t环形缓冲区由TIM2中断触发ADC读取并由主循环消费// adc.c #define ADC_BUFFER_SIZE 128 static uint16_t adc_buffer[ADC_BUFFER_SIZE]; static volatile uint16_t adc_head 0; // 下一个写入位置 static volatile uint16_t adc_tail 0; // 下一个读取位置 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { // 在TIM2中断中仅做最轻量操作启动ADC转换 HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 1); // 阻塞等待完成因采样快此方式比DMA更可控 uint16_t val HAL_ADC_GetValue(hadc1); // 写入环形缓冲区无锁因仅中断写、主循环读 adc_buffer[adc_head] val; adc_head (adc_head 1) % ADC_BUFFER_SIZE; // 若缓冲区满覆盖最老数据牺牲精度保实时性 if (adc_head adc_tail) adc_tail (adc_tail 1) % ADC_BUFFER_SIZE; } }逻辑说明中断内不处理数据只写缓冲区避免中断嵌套风险adc_head adc_tail表示缓冲区空adc_head (adc_tail1)%SIZE表示满未使用HAL_Delay()类阻塞函数所有延时由TIM2中断驱动确保时间基准稳定。3. 波形渲染引擎坐标变换、抗锯齿与帧同步策略3.1 坐标映射算法把12-bit ADC值压缩到LCD Y轴ADC原始值范围为0~409512位但LCD高度仅160像素直接线性映射会导致微小波动不可见。本项目采用“动态缩放偏移”策略使波形始终居中显示// waveform.c #define LCD_WIDTH 120 #define LCD_HEIGHT 160 #define ADC_MAX 4095 int16_t map_adc_to_y(uint16_t adc_val) { static uint16_t y_offset LCD_HEIGHT / 2; // 初始Y中心 static uint16_t y_scale 100; // 初始缩放因子越大越“压缩” // 每10帧动态调整缩放计算当前缓冲区极差使波形占满80%高度 if (frame_count % 10 0) { uint16_t min_val 4095, max_val 0; for (int i 0; i ADC_BUFFER_SIZE; i) { uint16_t v adc_buffer[i]; if (v min_val) min_val v; if (v max_val) max_val v; } uint16_t range max_val - min_val; if (range 100) { // 避免噪声干扰 y_scale (LCD_HEIGHT * 80 / 100) * 100 / (range 1); // 乘100防整除丢失精度 y_offset LCD_HEIGHT / 2 - (min_val range/2) * y_scale / 100; } } // 计算Y坐标y y_offset (adc_val - center) * y_scale / 100 int32_t center 2048; // ADC中点 int32_t y (int32_t)y_offset ((int32_t)adc_val - center) * y_scale / 100; // 限幅确保Y在0~LCD_HEIGHT-1范围内 if (y 0) y 0; if (y LCD_HEIGHT) y LCD_HEIGHT - 1; return (int16_t)y; }参数说明y_scale单位为“百分比缩放系数”例如y_scale50表示ADC每变化100点Y轴移动50像素y_offset动态补偿直流偏置使波形中心对齐LCD中线整个映射过程无浮点运算全部用定点整数实现符合裸机性能要求。3.2 实时绘图双缓冲机制与增量刷新直接在LCD上逐点画线会导致严重闪烁。本项目采用“内存缓冲区差异更新”策略先在RAM中构建120×160的uint8_t位图1字节1像素0黑1白再对比前后两帧仅刷新变化的列// framebuffer.c #define FB_WIDTH 120 #define FB_HEIGHT 160 static uint8_t frame_buffer[FB_HEIGHT][FB_WIDTH]; // 行优先存储 static uint8_t last_frame[FB_HEIGHT][FB_WIDTH]; void waveform_render() { // 步骤1清空当前帧缓冲区全黑 memset(frame_buffer, 0, sizeof(frame_buffer)); // 步骤2绘制波形线从左到右连接相邻点 for (int x 0; x FB_WIDTH - 1; x) { uint16_t idx (adc_tail x) % ADC_BUFFER_SIZE; uint16_t next_idx (adc_tail x 1) % ADC_BUFFER_SIZE; int16_t y1 map_adc_to_y(adc_buffer[idx]); int16_t y2 map_adc_to_y(adc_buffer[next_idx]); // Bresenham直线算法绘制两点间线段 draw_line(x, y1, x 1, y2); } // 步骤3对比last_frame与frame_buffer仅刷新变化的列 for (int x 0; x FB_WIDTH; x) { bool changed false; for (int y 0; y FB_HEIGHT; y) { if (frame_buffer[y][x] ! last_frame[y][x]) { changed true; break; } } if (changed) { // 刷新第x列设置窗口为x~x, 0~FB_HEIGHT-1然后写入整列数据 lcd_set_window(x, 0, x, FB_HEIGHT - 1); for (int y 0; y FB_HEIGHT; y) { uint8_t color frame_buffer[y][x] ? 0xFFFF : 0x0000; // 白/黑 lcd_write_data(color 8); lcd_write_data(color 0xFF); } } } // 步骤4交换缓冲区指针memcpy太慢直接交换地址 uint8_t (*temp)[FB_WIDTH] last_frame; last_frame frame_buffer; frame_buffer temp; }逻辑说明draw_line()使用经典Bresenham算法避免浮点除法差异检测按列进行因LCD SPI写入以列为单位最高效缓冲区交换用指针别名而非memcpy()节省约3ms CPU时间。3.3 帧同步与抗抖动让波形“稳住”的三个技巧裸机环境下ADC采样、TIM中断、LCD刷新三者不同步必然导致波形跳动。本项目通过以下三层过滤硬件滤波在PA0引脚串联100nF电容至GND抑制高频噪声软件中值滤波ADC读取后取连续3次采样中值非平均值消除脉冲干扰帧锁定机制waveform_render()只在TIM2中断标志置位后执行且执行完毕立即清除标志确保每帧严格对应1ms周期。注意若观察到波形左右漂移大概率是ADC采样未与TIM2中断严格对齐。检查HAL_TIM_Base_Start_IT(htim2)是否在MX_ADC1_Init()之后调用顺序错误会导致首次采样延迟。4. 避坑指南那些让我重焊三次排线的典型问题4.1 现象LCD全屏白/黑/花屏但SPI通信日志显示正常原因ST7735初始化序列中0xB1Frame Rate Control寄存器参数错误。常见误区是直接复制网上代码但不同批次ST7735芯片对0xB1的第二参数Gate Control敏感——部分芯片要求0x01部分要求0x02。解决在lcd_init()中定位lcd_write_cmd(0xB1);后两行尝试将lcd_write_data(0x01);改为lcd_write_data(0x02);重启观察。若仍无效查阅所用LCD模块背面丝印搜索对应型号的初始化文档。4.2 现象波形显示正常但Y轴随时间缓慢上移漂移原因map_adc_to_y()中动态y_offset计算未考虑ADC参考电压温漂。当板子运行10分钟后VREF因发热下降约2%导致相同电压对应ADC值增大y_offset持续负向修正。解决在map_adc_to_y()开头添加温度补偿项。若开发板有内部温度传感器如STM32F103的TS读取温度后查表修正y_offset (temp_now - 25) * 3;每℃补偿3像素。4.3 现象ADC采样值固定为0或4095串口打印显示HAL_ADC_GetValue()返回异常原因PA0引脚被意外配置为GPIO_MODE_OUTPUT_PP推挽输出而非ANALOG模式。HAL库初始化时若未显式设置GPIO_MODE_ANALOG某些CubeMX版本会默认为输入模式导致ADC无法采样。解决检查MX_GPIO_Init()中PA0配置强制添加GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_ANALOG; // 必须显式声明 GPIO_InitStruct.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);4.4 现象波形刷新卡顿示波器测得TIM2中断间隔忽长忽短原因HAL_ADC_PollForConversion()在中断中调用但未关闭全局中断__disable_irq()导致高优先级中断抢占使ADC读取超时。解决在HAL_TIM_PeriodElapsedCallback()中ADC操作前后加临界区__disable_irq(); HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 1); uint16_t val HAL_ADC_GetValue(hadc1); // ...写入缓冲区 __enable_irq();4.5 现象波形线条断裂相邻点之间出现明显空隙原因draw_line()实现有缺陷。常见错误是使用float计算斜率但在裸机环境下float运算极慢导致waveform_render()超时被下一次TIM2中断打断。解决改用纯整数Bresenham算法。参考标准实现确保所有变量为int16_t无除法、无浮点。重点检查dx, dy符号判断和误差项更新逻辑。5. 二次开发实战从“能显示”到“真可用”的四步改造5.1 第一步增加触控交互——用3个GPIO实现简易菜单LCD本身无触控但可通过扩展XPT2046芯片SPI接口或更低成本方案在LCD边框粘贴铜箔条接至STM32的3个GPIO如PC0/PC1/PC2配置为外部中断输入。当手指按压铜箔对应GPIO被拉低触发中断// touch.c void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin GPIO_PIN_0) { // PC0按下 → 放大Y轴 y_scale (y_scale 200) ? y_scale 20 : 200; } else if (GPIO_Pin GPIO_PIN_1) { // PC1按下 → 缩小Y轴 y_scale (y_scale 20) ? y_scale - 20 : 20; } else if (GPIO_Pin GPIO_PIN_2) { // PC2按下 → 冻结波形 freeze_waveform !freeze_waveform; } }验证方法用万用表蜂鸣档测试铜箔与GPIO引脚导通性确认按压时电压从3.3V跌至0V。无需校准3个物理按键足够实现核心功能切换。5.2 第二步支持多通道——复用ADC1的其他通道STM32F103的ADC1支持16个通道本项目仅用CH0。要显示双通道波形如CH0电压CH1电流需修改ADC配置为扫描模式并扩展缓冲区// 修改adc.c #define ADC_CHANNELS 2 static uint16_t adc_buffer[ADC_CHANNELS][ADC_BUFFER_SIZE]; static volatile uint16_t adc_head[ADC_CHANNELS] {0}; static volatile uint16_t adc_tail[ADC_CHANNELS] {0}; // 在HAL_TIM_PeriodElapsedCallback中轮询通道 static uint8_t current_channel 0; HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 1); adc_buffer[current_channel][adc_head[current_channel]] HAL_ADC_GetValue(hadc1); // ...更新head/tail current_channel (current_channel 1) % ADC_CHANNELS; HAL_ADC_SetChannel(hadc1, current_channel); // 切换通道关键点HAL_ADC_SetChannel()必须在每次采样前调用且需确保ADC已就绪加HAL_ADC_IsEnabled()检查。5.3 第三步导出数据到SD卡——用FatFs实现一键保存添加MicroSD卡槽SPI2接口移植FatFs R0.12b。核心是将环形缓冲区数据以CSV格式写入文件// sd_export.c FIL fil; // FatFs文件对象 f_open(fil, WAVE.CSV, FA_CREATE_ALWAYS | FA_WRITE); for (int i 0; i ADC_BUFFER_SIZE; i) { uint16_t idx (adc_tail i) % ADC_BUFFER_SIZE; char line[32]; sprintf(line, %d,%d\r\n, i, adc_buffer[idx]); f_write(fil, line, strlen(line), bw); } f_close(fil);血泪经验SD卡初始化失败率高达30%务必在f_mount()后加100ms延时并检查f_mkfs()返回值。建议首次使用前用PC格式化为FAT32。5.4 第四步接入上位机——用USB虚拟串口实时传输波形STM32F103无原生USB但可通过CH340G芯片转接。在main()循环中当检测到串口有数据到达时发送当前缓冲区快照// usb_transmit.c if (HAL_UART_Receive(huart1, rx_byte, 1, 1) HAL_OK) { if (rx_byte D) { // 接收D命令发送数据 for (int i 0; i 128; i) { uint16_t val adc_buffer[(adc_tail i) % 128]; HAL_UART_Transmit(huart1, (uint8_t*)val, 2, 100); } } }验证技巧用Python的pyserial库接收struct.unpack(H, data)解析16位值用matplotlib.animation.FuncAnimation实现实时绘图形成闭环调试系统。从那以后我每次新增外设都强制走一遍“硬件连接→引脚配置→时钟使能→外设初始化→中断注册→状态轮询”七步 checklist哪怕只是点亮一个LED。因为嵌入式里90%的问题根源都在初始化顺序或引脚复用冲突上而不是算法本身。希望帮到你。本文还有配套的精品资源点击获取