ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

STM32雷达扫描项目实战:舵机、超声波与TFT屏的嵌入式融合

STM32雷达扫描项目实战:舵机、超声波与TFT屏的嵌入式融合 简介这是一套基于STM32F103C8T6的嵌入式雷达系统实战项目资源面向嵌入式初学者与课程设计学生解决超声波测距、舵机协同控制及TFT-LCD图形化显示等典型综合开发问题。资源包共1024个文件涵盖563个C源码含驱动与算法实现、250个头文件定义外设配置与接口、51个汇编启动文件及28个IAR工程配置文件另有PDF原理图与Keil/IAR双平台可运行工程.uvprojx/.icf总大小23.84MB。已有270人学习下载体现其在教学实践与毕业设计中的实用热度。用户可直接获取完整雷达扫描逻辑包括定时器PWM精准驱动SG90舵机180°往复扫描、HC-SR04超声波测距Timer1触发Timer2捕获、SPI驱动2.4寸TFT-LCD实时绘制极坐标雷达图并在屏幕上动态标定物体角度与距离同时集成ARM CMSIS-DSP数学库相关模块如arm_rfft_init_f32、arm_dct4_init_q15等便于后续扩展信号处理功能。1. 项目缘起与核心目标最近在整理工作室的旧项目时翻出了一个基于STM32F103C8T6的雷达扫描原型。这个项目最初是为了验证一个简单的避障或区域扫描想法核心就是用最便宜、最常见的硬件——SG90舵机、HC-SR04超声波模块和一块2.4寸TFT屏来实时绘制一个极坐标下的“雷达图”。当时用CubeMX快速搭建了框架但代码写得比较随意。这次我决定把它重新梳理一遍做成一个结构清晰、可复现的完整项目。很多朋友在入门嵌入式图形界面或传感器融合时都会觉得无从下手这个项目恰好涵盖了定时器、PWM、外部中断、SPI通信和基础图形算法是一个非常好的综合练习。它不追求复杂的算法和精美的UI而是聚焦于如何将几个简单模块的数据流有机结合起来在屏幕上直观呈现这对于理解嵌入式系统的“输入-处理-输出”闭环非常有帮助。整个系统的逻辑很直观STM32F103C8T6作为大脑驱动SG90舵机进行0-180度的周期性扫描。在每一个角度上单片机触发HC-SR04超声波模块测距获取前方障碍物的距离信息。随后单片机需要将这对极坐标数据角度、距离转换为能在320x240像素的TFT-LCD屏幕上显示的直角坐标点并实时更新形成动态的扫描线和新旧点的更替最终呈现出一个类似雷达扫描的扇形画面。这里面有几个关键点需要解决如何精确控制舵机角度并与测距时序同步如何高效地在TFT屏上画点、画线并管理图形刷新以避免闪烁如何组织代码使得逻辑清晰且易于调试接下来我就结合CubeMX配置和HAL库编程一步步拆解实现过程。2. 硬件选型与CubeMX基础工程搭建2.1 核心控制器与开发环境选择主控芯片选择了经典的STM32F103C8T6也就是常说的“蓝莓派”或最小系统板核心型号。它拥有72MHz的Cortex-M3内核、64KB Flash和20KB RAM对于本项目来说性能绰绰有余。其丰富的外设特别是多个定时器和通信接口是本项目顺利实施的关键。开发环境方面STM32CubeMX是必不可少的起点。它极大地简化了引脚分配、时钟树配置和外设初始化的工作能生成兼容性良好的HAL库代码框架让我们可以专注于应用逻辑。首先在CubeMX中新建工程选择STM32F103C8T6。关键的系统配置是时钟树。我们需要让系统运行在最高性能通常将HSE外部高速时钟设为8MHz经过PLL倍频到72MHz作为系统时钟SYSCLK。APB1总线时钟定时器2、3、4等所在最高36MHzAPB2总线时钟高级定时器1、SPI1等所在就是72MHz。正确的时钟配置是后续PWM频率计算、SPI通信速率设定的基础。2.2 外设引脚分配与功能规划根据硬件连接我们需要在CubeMX中为每个外设分配具体的GPIO引脚。以下是核心外设的配置思路SG90舵机 (PWM控制)SG90舵机的控制信号线需要接在一个能输出PWM的引脚上。我们使用定时器TIM的PWM输出功能。通常选择TIM1、TIM2、TIM3、TIM4等通用或高级定时器。例如我将舵机信号线接在PA8引脚上这个引脚对应TIM1的通道1CH1。在CubeMX中找到TIM1将Channel1模式设为“PWM Generation CH1”。舵机的控制频率通常是50Hz周期20ms我们需要计算产生这个频率和对应脉宽的参数。HC-SR04超声波模块 (GPIO与定时器)HC-SR04需要两个GPIO一个用于触发Trig一个用于回响Echo。Trig配置为推挽输出模式即可。Echo信号则需要测量高电平的持续时间这个时间对应了距离。最精准的方法是使用输入捕获功能。我们可以将一个定时器的输入捕获通道连接到Echo引脚。例如将Echo接在PA0TIM2的通道1或PA6TIM3的通道1。在CubeMX中将对应TIM的Channel设为“Input Capture direct mode”。同时还需要一个基本的定时器如TIM6来提供超声波测量的超时判断防止因未收到回波而卡死。2.4寸 TFT-LCD屏 (SPI通信)市面上常见的2.4寸TFT屏如ILI9341驱动芯片大多采用SPI接口通信。我们需要配置一个SPI外设如SPI1或SPI2用于发送命令和数据。通常需要以下引脚SPI_SCK时钟、SPI_MOSI主机输出、SPI_CS片选GPIO模拟、DC数据/命令选择GPIO模拟、RESET复位GPIO模拟。背光控制BL也可以用另一个GPIO控制。在CubeMX中使能SPI1模式设为“Full-Duplex Master”硬件NSS片选设为“Disable”因为我们用软件控制CS。数据大小8位或16位取决于屏驱动芯片ILI9341通常用8位。然后将SCKPA5、MOSIPA7等引脚自动配置好。其他的控制引脚CS, DC, RESET, BL配置为普通的GPIO输出模式。调试与下载 (USART)强烈建议预留一个USART如USART1用于串口打印调试信息这将在开发过程中帮你大忙。配置为异步模式波特率115200。完成所有引脚分配后在Project Manager页面设置好工程名称、路径、IDEMDK-ARM或STM32CubeIDE等以及生成初始化代码的选项。关键点是在“Code Generator”标签页勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”这会让每个外设的代码更独立方便管理。3. 舵机角度控制与扫描逻辑实现3.1 SG90舵机PWM参数计算与配置SG90舵机的控制信号是一个周期为20ms50Hz脉宽在0.5ms到2.5ms之间的PWM波。脉宽对应舵机转动的角度通常0.5ms对应0度2.5ms对应180度这是一个线性关系。在CubeMX配置的TIM1中我们需要设置预分频器PSC和自动重载值ARR来产生20ms的周期。假设系统时钟APB2为72MHz定时器时钟也是72MHz如果APB预分频系数不为1定时器时钟会倍频这里假设为72MHz。目标频率50Hz - 周期 T 1/50 0.02s 20,000 us。定时器计数一次的时间t 1 / 72MHz ≈ 0.0139 us。需要计数的次数ARR_Counts T / t 20,000 us / 0.0139 us ≈ 1,440,000。这个值远大于16位定时器的最大值65535因此我们需要使用预分频器来降低定时器的计数频率。我们可以先设定一个合适的ARR值比如20000这样每个计数代表1us方便计算脉宽。那么需要的定时器时钟频率应为F_timer ARR / T 20000 / 0.02s 1,000,000 Hz 1MHz。预分频系数 PSC 72MHz / 1MHz - 1 71。所以在CubeMX的TIM1配置中我们将PrescalerPSC设为71Counter PeriodARR设为20000-119999因为从0开始计数。在PWM生成通道的设置里将Pulse初始脉宽设为对应0度角的值比如15001.5ms中间位置。生成代码后在main.c的初始化部分我们需要启动PWM输出HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1);3.2 角度扫描算法与定时器中断驱动我们需要让舵机在0到180度之间匀速来回扫描。一种简单的方法是使用一个定时器中断如TIM2每隔一个固定的时间比如20ms更新一次舵机的目标角度。首先定义一个全局变量servo_angle范围0-180。在定时器中断回调函数中例如HAL_TIM_PeriodElapsedCallback修改这个角度值并计算对应的PWM脉宽。脉宽计算函数如下// 将角度0-180转换为PWM比较值对应0.5ms-2.5ms // ARR19999 周期20000对应20ms 1us对应1个计数 // 0.5ms - 500计数 2.5ms - 2500计数 uint32_t angle_to_compare(uint16_t angle) { // 线性映射 angle(0~180) - compare(500~2500) return 500 (angle * (2500 - 500)) / 180; }在定时器中断中void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2.Instance) { // 假设TIM2用于产生扫描中断 static int8_t direction 1; // 扫描方向1为增-1为减 static uint16_t current_angle 0; current_angle direction; if (current_angle 180 || current_angle 0) { direction -direction; } uint32_t pulse angle_to_compare(current_angle); __HAL_TIM_SET_COMPARE(htim1, TIM_CHANNEL_1, pulse); // 更新PWM脉宽 // 记录当前角度用于后续与超声波测距同步 global_scan_angle current_angle; } }这里的关键是舵机转动需要时间SG90大约0.1秒/60度所以中断周期不能太短否则舵机跟不上指令。20ms的更新周期是一个比较平衡的选择既能平滑运动又不会太慢。同时这个global_scan_angle变量就是当前雷达扫描线所在的角度是后续进行测距和绘图的“角度基准”。4. 超声波测距模块的精准时序捕获4.1 HC-SR04工作原理与硬件连接HC-SR04模块有四个引脚VCC、GND、Trig触发、Echo回响。工作流程是单片机给Trig引脚一个至少10us的高电平脉冲模块会自动发出8个40kHz的超声波脉冲并检测回波。当检测到回波后Echo引脚会输出一个高电平其持续时间与距离成正比。距离计算公式为距离 (高电平时间 * 声速) / 2。声速在常温下可取340m/s或343m/s。硬件连接上VCC接5V或3.3V注意有些模块Echo输出也是5V电平可能需要电平转换但STM32F103的很多IO容忍5V具体看芯片手册GND接地。Trig接一个普通的GPIO输出口如PA1。Echo接配置为输入捕获模式的定时器通道引脚如PA0对应TIM2_CH1。4.2 使用输入捕获模式测量高电平时间在CubeMX中我们已将Echo对应的定时器通道例如TIM2的Channel1配置为输入捕获模式。生成代码后需要使能输入捕获中断并编写回调函数。首先在main.c的初始化部分启动输入捕获HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_1); // 启动TIM2通道1的输入捕获并使能中断然后我们需要处理捕获中断。HAL库提供了HAL_TIM_IC_CaptureCallback回调函数。思路是在上升沿捕获时记录时间戳T1在下降沿捕获时记录时间戳T2。差值(T2-T1)就是高电平时间。定时器的计数频率需要知道我们之前设置TIM2可能用于扫描这里我们可以用另一个定时器如TIM3专门做输入捕获或者复用TIM2但用不同的通道。为了简化假设我们用TIM3的通道1做输入捕获。在回调函数中void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM3.Instance htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { static uint32_t capture_start 0; static uint32_t capture_end 0; if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC1) ! RESET) { // 检查CC1中断标志 __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1); if (__HAL_TIM_GET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1) TIM_INPUTCHANNELPOLARITY_RISING) { // 上升沿捕获 capture_start HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 将捕获极性切换为下降沿 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING); } else { // 下降沿捕获 capture_end HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); // 计算时间差需要考虑计数器溢出的情况 uint32_t pulse_width; if (capture_end capture_start) { pulse_width capture_end - capture_start; } else { // 计数器溢出 pulse_width (htim-Instance-ARR - capture_start 1) capture_end; } // 计算距离单位厘米 // 定时器时钟假设为1MHz则1个计数1us // 距离 (时间us * 声速cm/us) / 2 (pulse_width * 0.0343) / 2 global_current_distance (pulse_width * 343) / 20000; // 简化计算: 0.0343/2 * pulse_width // 一次测量完成切换回上升沿捕获准备下一次 __HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING); } } } }4.3 触发测距与数据同步我们需要在舵机转动到每个特定角度时触发一次超声波测距。最简单的同步方法是在舵机角度更新的定时器中断里在更新完PWM脉宽后触发一次HC-SR04。在舵机扫描中断函数中增加触发逻辑void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2.Instance) { // ... 之前的舵机角度更新和PWM设置代码 ... // 触发一次超声波测距 HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_SET); delay_us(12); // 延时至少10us这里用12us HAL_GPIO_WritePin(TRIG_GPIO_Port, TRIG_Pin, GPIO_PIN_RESET); // 此时输入捕获中断会异步地测量Echo高电平时间并更新 global_current_distance // 我们需要将当前角度和距离“打包”供显示线程使用 if (global_current_distance 0 global_current_distance MAX_DETECT_RANGE_CM) { // 将数据存入一个缓冲区或直接用于绘图 radar_update_point(global_scan_angle, global_current_distance); } // 清空当前距离等待下一次测量 global_current_distance 0; } }这里有一个重要的细节超声波模块发出波束后需要一段时间才能收到回波飞行时间。在近距离时这个时间很短但在远距离比如4米时可能需要超过23ms340m/s下4米往返约23.5ms。而我们的舵机扫描中断周期是20ms如果每次中断都等待测距完成会严重拖慢扫描速度导致动画卡顿。因此必须采用异步非阻塞的方式。上面的代码正是如此触发后立即返回测距结果由输入捕获中断异步更新。我们使用一个全局变量global_current_distance来传递结果。但要注意数据竞争问题如果显示线程和中断同时读写这个变量可能会出错。一个简单的办法是在舵机中断中触发测距后复制角度和距离到一个受保护的结构体中例如使用临界区保护或者使用一个小的循环队列。注意HC-SR04有两个测量盲区大约2cm和450cm左右。在代码中需要加入有效范围判断例如只处理2cm到400cm之间的数据。另外声速受温度影响如果要求精度高可以加入温度传感器进行补偿。5. TFT-LCD驱动与雷达界面绘制5.1 SPI通信与屏驱初始化我们使用的2.4寸屏通常由ILI9341、ST7789等芯片驱动。这里以ILI9341为例。首先需要编写底层的SPI发送函数。HAL库提供了HAL_SPI_Transmit但为了效率我们通常直接操作寄存器或使用DMA。对于初始化阶段用HAL库函数即可。我们需要封装几个基本函数LCD_WriteCommand(uint8_t cmd): 发送命令DC引脚拉低。LCD_WriteData(uint8_t data): 发送数据DC引脚拉高。LCD_WriteData16(uint16_t data): 发送16位数据颜色数据常用。初始化流程就是按照ILI9341数据手册依次发送一系列初始化命令和参数来设置伽马值、颜色模式、显示方向、打开显示等。这部分代码通常比较固定可以从厂商例程或开源库中找到。一个关键设置是显示方向Rotation。我们需要将屏幕设置为横屏模式并且确定原点0,0的位置。对于雷达图我们通常希望屏幕中心作为雷达扫描的原点极坐标极点。假设屏幕是320x240我们可以将原点设在(160, 120)。设置显示方向时要确保X和Y轴的增长方向符合我们的绘图习惯。5.2 极坐标转换与画点函数雷达扫描得到的是极坐标数据角度θ和距离r。我们需要将其转换为屏幕上的直角坐标x, y。转换公式为x r * cos(θ) center_xy r * sin(θ) center_y这里有一个细节数学上的角度通常以X轴正方向为0度逆时针旋转为正。而我们的舵机扫描0度可能对应物理上的最左端。我们需要建立一个映射关系。假设舵机0度对应雷达图的-90度左侧180度对应90度右侧那么扫描角度servo_angle0-180对应的数学角度theta (servo_angle - 90) * π / 180。在嵌入式系统中直接计算浮点数的sin/cos比较耗时。我们可以采用查表法。预先计算好0到180度或者-90到90度每个整数角度对应的sin和cos值放大一定倍数如乘以256后存入数组使用时进行整数运算和移位可以大幅提高速度。// 预计算sin表角度范围0-180度对应实际角度-90到90度 #define SIN_SCALE 256 int16_t sin_table[181]; void init_trig_table(void) { for (int i 0; i 180; i) { float theta (i - 90) * 3.1415926f / 180.0f; sin_table[i] (int16_t)(sinf(theta) * SIN_SCALE); // cos(i) sin(i90)可以复用sin表 } } // 极坐标转直角坐标简化版假设r以像素为单位 void polar_to_cartesian(uint16_t angle, uint16_t distance_cm, uint16_t *x, uint16_t *y) { // 将距离厘米转换为屏幕像素半径。需要根据实际探测范围和屏幕大小设定比例尺。 // 例如最大探测距离400cm在屏幕上用200像素半径表示则 scale 200/400 0.5 像素/厘米 float scale 0.5f; uint16_t r_pixels (uint16_t)(distance_cm * scale); // 查表获取sin值cos通过sin表偏移90度获取 int16_t sin_val sin_table[angle]; int16_t cos_val sin_table[(angle 90) % 181]; // 注意边界 // 计算坐标使用整数运算提高速度 int32_t x_tmp (int32_t)r_pixels * cos_val / SIN_SCALE; int32_t y_tmp (int32_t)r_pixels * sin_val / SIN_SCALE; *x (uint16_t)(160 x_tmp); // 屏幕中心x160 *y (uint16_t)(120 - y_tmp); // 屏幕中心y120注意屏幕y轴向下为正所以减去y_tmp }有了坐标就可以写画点函数了。TFT画点通常需要先设置一个窗口即坐标范围然后连续写入颜色数据。对于单个点我们可以封装一个函数void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x 320 || y 240) return; // 边界检查 LCD_SetWindow(x, y, x, y); // 设置窗口为单个像素 LCD_WriteData16(color); // 写入该像素的颜色16位RGB565 }5.3 雷达扫描动态效果的实现雷达图的动态效果主要包括两部分一条从中心向外旋转的扫描线以及历史探测点的显示与消失模拟余晖效果。扫描线的绘制扫描线就是一条从中心点指向当前角度、长度为当前测距值的线段。我们可以用画线算法如Bresenham算法来绘制。但为了简单和速度也可以选择只画线段上的若干个点或者直接调用屏驱可能提供的画线函数如果底层驱动有。更简单的方法是在每次角度更新时擦除上一条扫描线绘制新的扫描线。擦除可以用背景色如黑色重画旧线。余晖效果为了让探测点不会立即消失我们可以维护一个点列表数组存储最近N次有效的探测点包含角度和距离或直接存储屏幕坐标。每次刷新屏幕时先清屏或绘制静态背景如刻度线、网格然后遍历这个列表用较暗的颜色绘制所有历史点最后用高亮颜色绘制最新的点。列表可以设定一个最大长度超过后覆盖最旧的点。另一种更节省内存的方法是“双缓冲绘图”在内存中维护一个代表屏幕的二维数组颜色索引或直接颜色值每次更新时将旧点的颜色衰减例如变暗绘制新点然后将整个内存数组刷新到屏幕上。但对于320x240分辨率全屏16位颜色需要150KB内存STM32F103C8T6的20KB RAM无法承受。因此维护一个有限的历史点列表是更可行的方案。一个简单的实现框架#define MAX_POINTS 100 typedef struct { uint16_t x; uint16_t y; uint32_t timestamp; // 可以用系统滴答计时器 uint8_t intensity; // 亮度用于实现衰减 } RadarPoint_t; RadarPoint_t point_list[MAX_POINTS]; uint16_t point_index 0; void radar_update_point(uint16_t angle, uint16_t distance) { uint16_t x, y; polar_to_cartesian(angle, distance, x, y); // 将新点存入列表 point_list[point_index].x x; point_list[point_index].y y; point_list[point_index].timestamp HAL_GetTick(); point_list[point_index].intensity 255; // 最高亮度 point_index (point_index 1) % MAX_POINTS; // 循环覆盖 } // 在定时器中断或主循环中调用此函数来刷新显示 void radar_draw_frame(void) { // 1. 清屏或绘制静态背景网格、刻度、文字 LCD_Clear(BLACK); // 2. 绘制所有历史点根据时间或强度衰减颜色 uint32_t current_time HAL_GetTick(); for (int i 0; i MAX_POINTS; i) { if (point_list[i].intensity 0) { // 简单的线性衰减每过100ms强度减50 uint32_t age current_time - point_list[i].timestamp; uint8_t decay (age / 100) * 50; if (decay point_list[i].intensity) { point_list[i].intensity 0; continue; } uint8_t current_intensity point_list[i].intensity - decay; // 将强度映射到颜色例如从绿色(0,255,0)衰减到深绿(0,64,0) uint16_t color RGB565(0, current_intensity, 0); LCD_DrawPoint(point_list[i].x, point_list[i].y, color); } } // 3. 绘制当前扫描线从中心到最新点 // 假设 latest_x, latest_y 是最近一次有效探测的坐标 if (latest_distance MAX_RANGE) { LCD_DrawLine(160, 120, latest_x, latest_y, GREEN); // 需要实现画线函数 } }6. 系统整合、优化与调试心得6.1 主循环与任务调度将上述所有模块整合起来main函数的结构通常如下int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_TIM1_Init(); // PWM for servo MX_TIM2_Init(); // Scan timer interrupt MX_TIM3_Init(); // Input capture for ultrasonic MX_SPI1_Init(); // For TFT-LCD MX_USART1_UART_Init(); // For debug init_trig_table(); LCD_Init(); LCD_Clear(BLACK); // 启动定时器中断 HAL_TIM_Base_Start_IT(htim2); // 舵机扫描定时器 HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); // 舵机PWM HAL_TIM_IC_Start_IT(htim3, TIM_CHANNEL_1); // 超声波输入捕获 while (1) { // 主循环负责非实时性的图形刷新 radar_draw_frame(); // 可以加入一个延时或基于系统滴答的帧率控制比如每秒刷新10-20帧 HAL_Delay(50); // 20 FPS } }这里采用了典型的前后台系统架构实时性要求高的任务舵机控制、超声波触发与捕获放在定时器中断中完成而图形刷新这种计算量相对较大、实时性要求稍低的任务放在主循环中。通过调整主循环中的延时可以控制雷达图的刷新频率。6.2 性能优化与常见问题排查屏幕闪烁如果radar_draw_frame中先清屏再画所有内容会出现明显的闪烁。优化方法是使用局部刷新。只擦除需要改变的区域如上一条扫描线而不是全屏清空。或者采用更复杂的“脏矩形”算法只更新发生变化的部分。测量数据抖动超声波测距本身有一定误差加上舵机机械振动可能导致同一个位置测出的距离值跳动。可以在软件中加入滤波算法。最简单的是一阶低通滤波filtered_distance alpha * new_distance (1 - alpha) * filtered_distance其中alpha是一个介于0和1之间的系数值越小越平滑但响应变慢。也可以使用中值滤波取最近几次测量的中值。舵机与测距不同步由于超声波测距是异步的可能出现“角度A触发测距但结果返回时舵机已经转到角度B”的情况。解决方法是给每个测距请求打上“角度标签”。在触发测距时将当前的global_scan_angle存入一个变量measurement_angle。当输入捕获完成得到距离后将(measurement_angle, distance)这个数据对存入点列表而不是使用实时的global_scan_angle。SPI通信速度瓶颈刷屏是主要的耗时操作。优化措施包括使用DMA传输颜色数据块。减少全屏刷新次数采用局部更新。使用屏驱的“写RAM”命令一次性发送多个像素数据而不是每个像素都设置一次窗口。内存不足STM32F103C8T6的20KB RAM很紧张。要避免大数组。历史点列表不宜过大如100个点。如果使用显存缓冲区可以考虑使用8位颜色索引调色板而非16位真彩色但这会增加代码复杂度。6.3 调试技巧与心得串口打印是王道在关键步骤如触发测距、捕获到边沿、计算完距离、坐标转换后通过串口打印出变量值是定位问题最快的方法。例如打印出pulse_width和计算出的distance可以验证超声波测量是否正常。利用LED和GPIO如果没有串口可以用一个GPIO引脚来产生脉冲信号用示波器或逻辑分析仪观察时序。例如在触发超声波时拉高一个引脚在收到回波时拉高另一个引脚可以直观看到整个测距周期和时序关系。分模块测试不要试图一次性把所有功能都调通。先单独测试舵机让它能平滑转动再单独测试超声波固定不动测量距离并在串口显示然后单独测试TFT屏画一些静态图形。最后再把它们组合起来。注意电源噪声舵机在转动时电流较大可能会引起电源电压波动影响超声波模块甚至单片机本身的稳定性。确保电源有足够的容量和良好的退耦在舵机和单片机电源引脚附近加足够大的电解电容和小的陶瓷电容。这个项目虽然硬件简单但软件上涵盖了中断、定时器、PWM、输入捕获、SPI通信、图形算法等多个嵌入式核心概念。把它一步步调试成功的过程本身就是对STM32和HAL库一次非常好的深入学习。最终看到屏幕上那个绿色的扇形扫描线一点点描绘出前方的障碍物轮廓时那种成就感正是嵌入式开发的乐趣所在。本文还有配套的精品资源点击获取
返回列表