STM32串口字符画:从图像处理到终端显示的嵌入式实践

发布时间:2026/6/5 15:22:19

STM32串口字符画:从图像处理到终端显示的嵌入式实践 1. 项目概述从点灯到“画图”探索MCU的趣味玩法拿到一块新的开发板比如ST的NUCLEO-F411RE很多工程师的第一反应可能就是点个灯、调个串口验证一下基础功能。这确实是标准流程但做完这些之后呢板子是不是就放一边吃灰了这次我想分享一个有点“不务正业”但非常有趣的小项目用STM32F411RET6的串口在终端上输出一幅由字符组成的图像。这听起来可能有点复古毕竟现在都是彩色液晶屏的时代了。但恰恰是这种在纯文本环境下的“图形化”尝试能让我们更深入地理解数据、显示原理以及底层驱动的灵活性。它不仅是简单的串口打印更是一次对图像数据格式、内存布局和实时流处理的微型实践。这个项目的核心价值在于它用一个非常具体的例子串联起了嵌入式开发中的几个关键环节开发环境搭建、外设驱动GPIO、UART、数据转换与处理。最终我们能在串口助手上看到由“A”和空格组成的“JUST DO IT”标语或者一对“炯炯有神”的眼睛。这个过程本身充满了极客的乐趣也能给项目调试、UI原型设计甚至艺术创作带来启发。无论你是刚接触STM32的新手想找一个比点灯更有成就感的练手项目还是经验丰富的老鸟想在工作之余找点乐子这个项目都值得一试。接下来我会基于标准外设库和IAR环境把整个过程掰开揉碎从环境搭建到图像转换再到代码实现和优化技巧毫无保留地分享出来。2. 开发环境搭建与工程框架解析虽然STM32CubeMX以其图形化配置和HAL库闻名大大降低了入门门槛但我个人在快速验证想法或进行深度优化时依然偏爱标准外设库。原因很简单直接、透明、可控。你能清楚地知道每一个寄存器被写入了什么值中断向量表是如何安排的库函数背后到底做了什么。这对于理解硬件原理和排查一些底层问题非常有帮助。因此本项目依然采用经典的STM32F4xx_DSP_StdPeriph_Lib_V1.8.0标准库作为基础。2.1 工具链选择与工程导入我使用的开发环境是IAR Embedded Workbench for ARM。选择IAR是因为其编译效率高生成的代码紧凑调试器集成度好。当然Keil MDK或者纯GCCMakefile也是完全可行的这里以IAR为例进行说明。首先你需要从ST官网下载标准外设库。解压后找到这个关键路径\en.stm32f4_dsp_stdperiph_lib\STM32F4xx_DSP_StdPeriph_Lib_V1.8.0\Project\STM32F4xx_StdPeriph_Templates\EWARM\。在这个目录下你会找到Project.eww文件这就是IAR的工程文件。双击打开工程后第一件要紧事是确认MCU型号。模板工程默认的型号可能不是F411。你需要右键点击工程名选择“Options”在“General Options” - “Target”选项卡中确认设备Device是否正确选择为“STM32F411xE”。这一步至关重要它决定了编译器使用的芯片内核头文件、内存映射以及启动文件。如果选错轻则编译报错重则代码运行异常。注意不同系列的STM32启动文件如startup_stm32f411xe.s和系统初始化文件system_stm32f4xx.c是不同的。务必确保工程中包含的启动文件与你的芯片型号完全匹配。标准库的模板通常已经包含了一系列启动文件你需要将正确的文件添加到工程并移除其他的。2.2 工程结构与关键配置一个典型的StdPeriph库工程结构包含以下核心部分User存放用户自己的主程序main.c、中断服务程序stm32f4xx_it.c和头文件stm32f4xx_conf.h。StdPeriph_Driver标准外设库的源文件和头文件。CMSISCortex微控制器软件接口标准文件包含内核相关的定义和函数。EWARMIAR特定的配置文件如链接脚本.icf和调试配置。编译前请打开stm32f4xx_conf.h文件。这个文件用于管理使用哪些外设库。例如我们需要用到GPIO和USART那么就要确保#define USE_STDPERIPH_DRIVER被启用并且#include “stm32f4xx_gpio.h”和#include “stm32f4xx_usart.h”没有被注释掉。同时检查stm32f4xx.h中关于芯片型号的宏定义通常是#define STM32F411xE。配置完成后点击编译。如果一切顺利你应该能得到一个零错误、零警告的编译结果。接下来连接你的NUCLEO-F411RE开发板它自带ST-LINK调试器点击下载并调试。程序应该能正常运行并在main函数的开始处停下。如果调试器能成功连接并停在main函数入口那么恭喜你最基础的开发环境已经搭建成功。这看似简单的几步却是后续所有工作的基石。3. 基础外设驱动从LED到串口在开始“画图”之前我们必须确保两个最基本的外设工作正常GPIO控制LED和USART进行串口通信。这不仅是功能验证也是理解STM32外设编程模式的必经之路。3.1 GPIO驱动点亮第一盏灯NUCLEO-F411RE板载了一个用户LED连接在PA5引脚上。通过原理图确认这一点非常重要我一开始就曾误操作了另一个LED引脚PB13连接在ST-LINK上导致现象诡异。点灯程序虽然简单但体现了STM32外设初始化的标准流程使能时钟、配置引脚模式、输出电平。首先任何外设使用前必须先使能其对应的总线时钟。对于GPIOA它挂载在AHB1总线上。因此我们需要调用RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE)。接下来定义一个GPIO_InitTypeDef结构体变量并填充其成员GPIO_Pin指定为GPIO_Pin_5。GPIO_Mode设置为GPIO_Mode_OUT即通用输出模式。GPIO_Speed输出速度可选低速、中速、高速、超高速。对于LEDGPIO_Speed_50MHz中速足矣。GPIO_OType输出类型推挽输出GPIO_OType_PP即可。GPIO_PuPd上拉/下拉。LED电路通常已有限流电阻这里选择GPIO_PuPd_NOPULL无上下拉。初始化完成后就可以用GPIO_SetBits(GPIOA, GPIO_Pin_5)和GPIO_ResetBits(GPIOA, GPIO_Pin_5)来控制LED的亮灭了。写一个简单的延时闪烁程序下载到板子看到LED有规律地闪烁GPIO驱动部分就完成了。3.2 USART驱动打通与PC的对话通道串口是嵌入式开发的“嘴巴”和“耳朵”调试信息输出、指令输入都离不开它。NUCLEO-F411RE板载的ST-LINK虚拟了一个COM端口通过USB连接到电脑对应的是USART2引脚是PA2TX和PA3RX。初始化USART的步骤比GPIO稍多但逻辑清晰使能时钟需要使能GPIOA用于TX/RX引脚和USART2的时钟。USART2挂载在APB1总线上。RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE);配置GPIO引脚复用功能PA2和PA3需要配置为复用功能模式。GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; // 复用模式 GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_OType GPIO_OType_PP; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_UP; // 内部上拉稳定电平 GPIO_Init(GPIOA, GPIO_InitStruct); // 将引脚映射到USART2功能 GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2);配置USART参数设置波特率、数据位、停止位、校验位等。USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate 115200; // 常用波特率 USART_InitStruct.USART_WordLength USART_WordLength_8b; USART_InitStruct.USART_StopBits USART_StopBits_1; USART_InitStruct.USART_Parity USART_Parity_No; USART_InitStruct.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_InitStruct.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART2, USART_InitStruct);使能USARTUSART_Cmd(USART2, ENABLE)。为了方便使用我们还需要实现一个简单的字符发送函数例如void USART2_SendChar(char ch)内部循环等待发送数据寄存器空USART_GetFlagStatus(USART2, USART_FLAG_TXE)然后写入数据。再基于此实现字符串发送函数void USART2_SendString(char *str)。最后在PC上打开串口助手如Putty、SecureCRT或MobaXterm选择正确的COM口设置波特率1152008N1。如果能在终端上收到“Hello World”或类似的测试信息那么串口通信的桥梁就稳固地搭建起来了。4. 趣味图像生成的核心原理与数据准备一切准备就绪现在进入最核心的部分如何把一张图片变成一串能在串口终端显示的字符。这个过程本质上是一个“图像二值化”和“数据编码”的过程。4.1 图像预处理从位图到二值数组我们的目标是输出黑白图像因此第一步是准备一张单色位图。你可以用任何简单的绘图工具比如Windows自带的“画图”软件。创建画布新建一个图像设置一个合适的尺寸。例如我选择了104像素宽32像素高。这个尺寸决定了最终输出在终端上的“分辨率”。宽度不宜超过终端窗口的字符宽度高度则决定了需要打印多少行。绘制内容用黑色前景色在白色背景上写下你想显示的文字或图案比如“JUST DO IT”。保存时关键一步来了必须将图片另存为“单色位图”.bmp格式。这个格式意味着每个像素只用1个比特bit表示0代表白色背景1代表黑色前景。这是我们后续处理的基础。实操心得在“画图”软件中保存为“单色位图”后图片会变得只有黑白两色没有任何灰度。如果保存为其他格式如24位位图每个像素会包含RGB三个通道的信息处理起来会复杂得多。确保是“单色”是简化后续所有步骤的关键。4.2 数据转换使用Image2Lcd工具单色位图准备好了但单片机需要的是可以直接使用的字节数组。这里推荐使用“Image2Lcd”这类小工具网上有很多类似软件。它的作用是将图片的像素数据按照我们指定的扫描方式和格式转换成C语言数组。打开软件并导入图片运行Image2Lcd打开刚才保存的单色位图。关键参数设置输出数据类型选择“C语言数组”或“十六进制文本”。扫描模式这决定了字节中比特的顺序与图片像素的对应关系。通常有“水平扫描”和“垂直扫描”。对于我们的应用逐行从左到右输出字符选择“水平扫描”即可。如果发现生成的图像方向不对可以尝试“垂直扫描”或勾选“字节内像素数据反序”等选项进行调整。输出灰度选择“单色”。最大宽度和高度确保不小于原图尺寸。其他选项如“反白”、“镜像”等可以根据想要的显示效果进行勾选实现负片或翻转效果。生成与保存点击“保存”将生成的数组保存为一个.h头文件例如picture.h。这个文件里会包含一个类似const unsigned char gImage_JustDoIt[] { ... };的数组定义。数组的每个元素一个字节对应图片上横向连续的8个像素假设水平扫描。例如一个字节0xF0二进制11110000表示连续的8个像素中前4个是黑色1后4个是白色0。4.3 数据格式解析与内存布局理解理解生成的数组结构至关重要。假设我们有一张8像素宽、N像素高的图片宽度最好是8的倍数便于处理。采用水平扫描那么数组的第一个字节对应图片第一行最左边的8个像素。第二个字节对应第一行接下来的8个像素以此类推直到第一行结束。然后紧接着是第二行的数据依此类推。因此数组的总长度 (图片宽度 / 8) * 图片高度。例如104x32的单色图宽度104不是8的倍数工具通常会补足到1128的倍数或保持104但按字节处理边界。我们需要知道工具实际生成的宽度字节数。在picture.h中通常会有注释或数组名暗示尺寸如gImage_JustDoIt[448]那么可以推算出图片高度 32 每行字节数 448 / 32 14字节 112比特。所以实际处理的图像宽度是112像素。5. 单片机端图像输出程序实现数据已经准备成C数组接下来就是在STM32上编写程序将这些数据“翻译”成终端上的字符画。5.1 核心算法比特映射到字符核心思路是遍历数组中的每一个字节再遍历该字节中的每一个比特从最高位MSB或最低位LSB开始取决于扫描设置。如果该比特为1则通过串口发送一个代表“黑点”的字符如‘A’、‘#’、‘*’如果为0则发送一个空格‘ ‘。每处理完一行数据即宽度方向的所有字节就发送一个换行符“\r\n”将光标移动到下一行开头。下面是一个简化的核心代码框架// 假设已知图像宽度像素img_width高度像素img_height // 以及每行所占的字节数 bytes_per_line (img_width 7) / 8; // 向上取整 const uint8_t *pixel_data gImage_JustDoIt; // 指向图像数组 void UART_PrintImage(void) { for (int h 0; h img_height; h) { // 遍历每一行 for (int byte_idx 0; byte_idx bytes_per_line; byte_idx) { // 遍历该行的每一个字节 uint8_t current_byte pixel_data[h * bytes_per_line byte_idx]; // 遍历该字节的8个比特这里假设MSB对应最左边的像素 for (int bit_idx 7; bit_idx 0; bit_idx--) { // 计算当前像素在整行中的位置防止超出图像实际宽度 int pixel_pos byte_idx * 8 (7 - bit_idx); if (pixel_pos img_width) { break; // 如果已超出图像实际宽度则跳出比特循环针对宽度非8倍数的情况 } if ((current_byte bit_idx) 0x01) { // 判断当前比特是否为1 USART_SendChar(‘A‘); // 发送实心字符 } else { USART_SendChar(’ ‘); // 发送空格 } } } // 一行结束发送换行 USART_SendString(“\r\n“); } }5.2 优化与增强滚动、动画与交互基础功能实现后我们可以玩出更多花样动态效果在main函数的循环中多次调用UART_PrintImage()并在每次打印前发送清屏指令如“\033[2J“或“\r\n\r\n...“模拟清屏可以实现简单的动画。例如让图像在屏幕上移动或者交替显示不同的图像。交互控制结合串口接收中断可以让PC端发送指令来选择显示哪幅图片或者控制动画的速度。数据压缩如果图片较大数组会占用可观的Flash空间。可以考虑使用简单的游程编码RLE压缩在输出时实时解压。例如连续10个空格可以用一个特殊标记加数字10来表示。字符艺术不一定只用一种字符。可以根据字节的值0-255映射到一个包含不同密度字符如‘ ‘, ‘.’, ‘:’, ‘*’, ‘#’, ‘’的查找表中实现灰度效果尽管终端是单色的但不同字符的视觉密度可以模拟出灰度层次。在我的实际测试中输出那对“眼睛”图像时由于图像细节较多最初直接发送导致串口缓冲区溢出图像错乱。解决方案是在发送每个字符后加入一个微小的延时如Delay_us(10)或者检查串口发送完成标志确保数据流稳定。另一个问题是终端窗口的自动换行可能破坏图像格式。务必确保终端窗口的宽度设置大于等于图像的“字符宽度”图像像素宽度并将终端的自动换行功能关闭。6. 常见问题排查与深度优化技巧在实际操作中你可能会遇到各种问题。下面我将一些典型问题及解决方案整理成表并分享几个提升效果和效率的技巧。问题现象可能原因排查步骤与解决方案终端无任何输出1. 串口线未接好或COM口选错。2. 波特率不匹配。3. 单片机程序未运行或卡死。1. 检查设备管理器中的端口号确认接线。2. 核对代码与终端软件的波特率、数据位、停止位、校验位是否完全一致。3. 用调试器单步执行检查是否能运行到串口发送函数。先发送一个简单的字符串测试。输出乱码1. 波特率误差过大。2. 终端字符编码不匹配如UTF-8 vs ANSI。3. 图像数组数据错误或指针越界。1. STM32的USART波特率发生器精度很高通常不是主因。检查时钟树配置确保系统时钟和APB总线时钟正确。2. 将终端软件编码设置为ASCII或ANSI。3. 检查picture.h数组数据用十六进制查看器对比原图工具预览。确保数组尺寸和访问索引计算正确。图像扭曲、错位1. 扫描方向MSB/LSB与程序解析顺序不一致。2. 图像宽度非8倍数边界处理错误。3. 终端字体不是等宽字体。1. 调整内层比特循环的顺序for(int bit_idx7; bit_idx0; bit_idx--)改为for(int bit_idx0; bit_idx8; bit_idx)或修改Image2Lcd中的“字节内像素数据反序”选项。2. 在程序中加入像素位置判断超出实际宽度时用空格填充或提前跳出。3. 将终端字体如Putty设置为“Consolas”或“Courier New”等等宽字体。输出图像上下颠倒Image2Lcd中的“垂直扫描”或“逆向输出”选项被误选。在Image2Lcd中调整扫描模式或是在程序输出时将行遍历顺序从0 to height-1改为height-1 to 0。输出速度慢动画卡顿1. 串口波特率过低如9600。2. 发送每个字符后使用阻塞延时。3. 未使用DMA或中断发送。1. 提高波特率到115200甚至更高确保终端和代码同步修改。2. 改用非阻塞方式检查USART状态寄存器发送完成标志TC或发送数据寄存器空标志TXE等待就绪后再发送下一个字符避免延时。3. 对于大数据量或高速动画可以考虑配置USART的DMA发送模式将整个图像行或帧数据通过DMA自动发送极大解放CPU。深度优化技巧实录双缓冲与流式输出对于动态图像可以开辟两个缓冲区。当DMA正在发送缓冲区A的数据时CPU可以准备下一帧数据到缓冲区B。发送完成后立即切换实现流畅动画。利用终端转义序列除了清屏(\033[2J)还可以使用光标定位序列如\033[row;colH直接在任何位置输出图像的一部分实现更复杂的图形界面效果而无需重绘整个屏幕。从Flash直接读取优化图像数组通常存放在Flash中。频繁读取时注意STM32的Flash访问速度。如果系统时钟很高可以考虑将常访问的数据拷贝到RAM中处理或者启用Flash的加速功能如ART加速器在F4系列是默认开启的。自定义字符集如果你使用的终端支持如某些嵌入式图形终端甚至可以自定义字符将多个像素组合成一个自定义字符从而用更少的数据量传输更丰富的图形信息。这个小项目虽然始于“趣味”但深入下去可以牵扯出嵌入式图形显示、数据压缩、实时流处理、人机交互等多个领域的知识点。它像一把钥匙打开了一扇门门后是一个将硬件操控与软件创意结合起来的广阔天地。下次当你调试一个没有屏幕的设备时不妨试试用串口给它“画”个状态图标既实用又有趣。

相关新闻