1. 系统需求分析与设计思路
蓝桥杯嵌入式省赛的电阻检测系统是一个典型的综合应用项目,需要选手在有限时间内完成从硬件连接到软件编程的全流程开发。这个系统要求我们能够准确测量两个电阻(R37和R38)的电压值,并根据预设的上下限判断电阻是否合格,同时还要实现LCD显示、按键控制和串口通信等功能。
我在实际开发中发现,这个项目的难点不在于单个模块的实现,而在于如何让各个模块协同工作。ADC采集需要稳定准确,按键处理要防抖可靠,LCD显示要实时更新,串口通信要高效稳定。这些模块之间存在着紧密的数据流动和控制逻辑,任何一个环节出现问题都会影响整个系统的性能。
从硬件连接来看,我们需要将两个电位器分别连接到ADC1和ADC2的通道上,按键连接到PB0、PB1、PB2和PA0,LED灯连接到PC8-PC12,LCD屏幕通过FSMC接口连接。这些硬件连接虽然看似简单,但在实际布线时很容易出错,特别是ADC的引脚分配和LCD的初始化配置,需要特别注意。
2. ADC电压采集模块实现
ADC模块是整个系统的核心,直接决定了电阻检测的准确性。STM32F103的ADC是12位精度的,参考电压为3.3V,所以每个ADC值对应的电压计算公式为:电压值 = (ADC值 × 3.3) / 4095。
在实际编程中,我采用了HAL库提供的ADC函数来实现电压采集:
void getR37value(void)
{
uint16_t ADC2_Value;
HAL_ADC_Start(&hadc2);
ADC2_Value = HAL_ADC_GetValue(&hadc2);
R37_volt = (ADC2_Value * 3.3f) / 4095.0f;
}
这个函数看起来简单,但里面有几个关键点需要注意。首先是ADC的启动时机,我选择在主循环中周期性地调用采集函数,这样既能保证数据的实时性,又不会过度占用CPU资源。其次是数据类型的处理,ADC值是16位整数,但电压值需要转换为浮点数进行计算,这里使用3.3f而不是3.3可以避免隐式类型转换带来的精度损失。
在实际测试中,我发现ADC读数存在一定的波动,特别是在电源电压不稳或者周围有电磁干扰的情况下。为了解决这个问题,我采用了软件滤波的方法,连续采集多次然后取平均值:
#define SAMPLE_TIMES 10
void getR37value_filtered(void)
{
uint32_t sum = 0;
for(int i = 0; i < SAMPLE_TIMES; i++) {
HAL_ADC_Start(&hadc2);
sum += HAL_ADC_GetValue(&hadc2);
HAL_Delay(1);
}
R37_volt = (sum * 3.3f) / (4095.0f * SAMPLE_TIMES);
}
这种方法虽然增加了采集时间,但显著提高了测量的稳定性和准确性。在实际比赛中,可以根据具体需求调整采样次数,在精度和速度之间找到平衡点。
3. 按键控制与状态管理
按键控制模块负责处理用户的输入操作,包括页面切换、电阻检测、参数调整等功能。系统设计了三个主要页面:检测页面、参数设置页面和合格率显示页面,通过按键B1进行切换。
按键处理中最常见的问题就是抖动,机械按键在按下和释放时会产生多次电平跳变,如果不进行处理会导致多次误触发。我采用了状态机的方式来实现按键消抖:
uint8_t key_value, key_down, key_up, key_old = 0;
void key_scan(void)
{
// 读取按键状态
if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == RESET)
key_value = 1;
else if(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == RESET)
key_value = 2;
// 其他按键类似...
else
key_value = 0;
// 检测按键按下和释放事件
key_down = key_value & (key_value ^ key_old);
key_up = ~key_value & (key_value ^ key_old);
key_old = key_value;
}
这种方法的巧妙之处在于通过位运算来检测状态变化,key_down只在按键从0变为1时有效,key_up只在按键从1变为0时有效。配合20ms的消抖延时,可以可靠地检测按键事件而不受抖动影响。
在实际应用中,我还增加了长按功能的支持。通过记录按键按下的时间,当超过一定阈值时触发长按事件,这样可以实现更丰富的交互功能。比如在参数设置页面,长按按键可以快速调整数值,提高操作效率。
状态管理是另一个重要方面,系统需要根据当前页面和用户操作来执行相应的功能。我使用page_index变量来记录当前页面,line_index来记录参数设置页面中的选中行,这样就能清晰地管理整个系统的状态流转。
4. LCD显示界面设计
LCD显示模块负责将系统的各种状态信息直观地展示给用户。根据题目要求,我们需要实现三个显示页面,每个页面显示不同的信息内容。
在显示编程中,最重要的是保证显示内容的清晰和实时性。我采用了200ms的刷新周期,这个时间间隔既能保证显示的流畅性,又不会给系统带来过重的负担。显示内容通过sprintf函数格式化为字符串,然后调用LCD驱动函数显示:
char buf[30];
u32 lcd_tick = 0;
void lcd_pro(void)
{
if(uwTick - lcd_tick < 200) return;
lcd_tick = uwTick;
switch(page_index) {
case 1: // 检测页面
LCD_DisplayStringLine(Line1, (u8 *)" GOODS ");
sprintf(buf, " R37:%.2lfV ", R37_volt);
LCD_DisplayStringLine(Line3, (u8 *)buf);
// 其他行类似...
break;
// 其他页面...
}
}
在实际开发中,我发现LCD显示有几个需要注意的地方。首先是内存使用,频繁的字符串操作容易导致内存碎片,所以我使用了静态分配的缓冲区。其次是显示优化,避免频繁清屏造成的闪烁感,只更新需要变化的部分。
为了提升用户体验,我还增加了一些视觉反馈。比如在参数设置页面,当前选中的参数会反色显示;在检测页面,合格和不合格的电阻会用不同的颜色标识。这些细节虽然小,但能显著提高系统的专业感和易用性。
显示内容的布局也很重要,重要的信息应该放在显眼的位置,数值显示要保留合适的小数位数,单位要明确标注。这些设计考虑都能让用户更快地获取需要的信息。
5. 串口通信优化策略
串口通信模块负责与上位机进行数据交互,发送电阻检测的结果。最初的实现存在几个严重问题:响应延迟大、不支持连续命令、抗干扰能力差,还有缓冲区溢出风险。
我通过以下几个方面的优化显著改善了串口通信的性能:
首先增加了接收缓冲区的大小,从原来的6字节扩大到64字节,这样就能处理更长的数据帧。其次引入了超时机制,当一段时间没有收到新数据时,就认为一帧数据接收完成,开始处理:
#define RX_BUF_SIZE 64
#define RX_TIMEOUT 100 // 100ms超时
uint8_t rx_pointer = 0;
uint8_t rx_data;
uint8_t rx_buf[RX_BUF_SIZE];
uint32_t last_rx_time = 0;
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(rx_pointer < RX_BUF_SIZE-1) {
rx_buf[rx_pointer++] = rx_data;
last_rx_time = uwTick;
}
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
这种设计的好处是能够自适应不同长度的数据帧,不会因为固定长度判断而丢失数据。超时时间设置为100ms,这个时间既不会太短导致误判,也不会太长影响响应速度。
在数据处理方面,使用memcmp函数进行命令匹配比逐个字符比较更加高效和可靠:
void rx_pro(void)
{
if((uwTick - last_rx_time > RX_TIMEOUT) && rx_pointer > 0) {
if(rx_pointer >= 3) {
if(memcmp(rx_buf, "R37", 3) == 0) {
printf("R37:%d,%d,%.1lf%%\r\n",
R37_Sum, R37_qualified_quantity, R37_yield);
}
// 其他命令处理...
}
rx_pointer = 0;
memset(rx_buf, 0, sizeof(rx_buf));
}
}
优化后的串口通信模块响应延迟从50ms降低到实时响应,支持连续命令处理,抗干扰能力显著提升,缓冲区溢出风险也完全消除。这些改进使得系统能够稳定可靠地与上位机进行数据交互。
6. 系统整合与性能调优
各个模块单独调试完成后,就需要将它们整合成一个完整的系统。整合过程中最重要的是处理好模块之间的数据流和控制流,避免资源冲突和逻辑错误。
在主循环中,我按照合理的顺序调用各个处理函数:
while (1) {
key_pro(); // 按键处理
lcd_pro(); // LCD显示
getR38value(); // ADC采集
getR37value();
led_pro(); // LED控制
if(rx_pointer != 0) {
rx_pro(); // 串口处理
}
}
这个顺序是经过仔细考虑的:先处理用户输入,然后更新显示,接着采集数据,最后处理通信。这样的安排保证了用户操作的及时响应,同时数据采集和显示也能保持较好的实时性。
在性能调优方面,我主要关注以下几个方面:首先是CPU利用率,通过合理设置各模块的处理频率,避免不必要的计算和等待。其次是内存使用,优化数据结构和缓冲区大小,减少内存碎片。最后是响应速度,确保系统对用户操作和外设事件都能及时响应。
中断处理也是需要特别注意的地方。系统中使用了定时器中断进行LED控制,串口中断进行数据接收,这些中断服务函数应该尽量简短,避免长时间占用中断资源。我在中断中只设置标志位,实际的处理放在主循环中执行,这样既能保证及时响应,又不会影响其他任务的执行。
电源管理是另一个优化点,特别是在电池供电的应用中。通过合理配置外设的功耗模式,在不需要的时候关闭或休眠,可以显著延长系统的工作时间。虽然在这个比赛中不是主要考核点,但良好的功耗设计习惯对实际项目很有帮助。
7. 常见问题与调试技巧
在开发过程中,我遇到了不少典型问题,这里分享一些解决方案和调试技巧。
ADC采集不准是一个常见问题,除了前面提到的软件滤波外,还要注意硬件方面的因素。电源电压的稳定性对ADC精度影响很大,最好使用稳定的基准电压源。模拟和数字部分的地线要分开布局,避免数字噪声干扰模拟信号。采样时间设置也很重要,要根据信号源阻抗选择合适的采样时间。
LCD显示异常往往与初始化序列有关。不同型号的LCD初始化参数可能不同,需要仔细查阅数据手册。显示内容乱码可能是字库问题或者数据传输错误,可以通过简化显示内容逐步排查。
按键失灵或者多次触发通常是消抖处理不当。除了软件消抖,还可以在硬件上增加电容滤波。按键扫描频率也很重要,太慢会影响响应速度,太快会增加CPU负担,一般10-20ms的扫描间隔比较合适。
串口通信问题比较复杂,可能涉及波特率设置、数据格式、流控制等多个方面。我习惯先用串口调试助手确认硬件连接正常,然后再调试软件部分。数据接收不完整可以检查中断优先级和缓冲区大小,数据错误可以检查波特率是否匹配。
调试过程中,LED灯是最简单的调试工具。通过控制不同的LED灯亮灭,可以直观地显示程序执行到哪个阶段,或者某些条件是否满足。串口打印也是重要的调试手段,可以输出详细的运行状态和变量值。
版本控制很重要,特别是在时间紧张的比赛中。我习惯在实现每个重要功能后都进行一次代码备份,这样当新引入的代码导致问题时,可以快速回退到稳定版本。
484



被折叠的 条评论
为什么被折叠?



