1. 项目概述
蓝桥杯嵌入式17届模拟3这道题目确实挺有意思的,它巧妙地将ADC采集和LCD显示结合起来,要求我们根据电位器旋转的角度在屏幕上实时显示一个简易的动画效果。作为一名参加过多次嵌入式竞赛的老手,我觉得这道题最大的价值在于训练我们对LCD底层操作的理解和实时数据处理的思维。
这道题的核心难点在于两点:一是如何精确控制LCD的每个像素点来呈现电位器的角度变化;二是如何处理两个电位器的实时数据采集与显示。相比之前常用的LCD_DisplayStringLine()这种整行显示的函数,这次我们需要更精细地控制显示内容,这就涉及到对LCD底层显示原理的深入理解。
2. LCD显示方案设计
2.1 显示函数选择
题目要求根据电位器角度实时修改屏幕的特定区域,这意味着我们需要精确控制显示位置。传统的LCD_DisplayStringLine()函数虽然简单易用,但它是一次性写入整行内容,无法满足精细控制的需求。
经过多次尝试,我最终选择了LCD_DisplayChar()函数。这个函数可以让我们以行号和字符位置为坐标,精确控制每个字符的显示位置。它的函数原型通常是这样的:
c复制void LCD_DisplayChar(uint8_t Line, uint16_t Column, uint8_t Ascii);
其中:
- Line参数指定行号(通常0-7或0-3,取决于LCD型号)
- Column参数指定列位置(像素坐标)
- Ascii参数是要显示的字符
2.2 字符串显示实现
由于LCD_DisplayChar()只能显示单个字符,要显示字符串就需要自己封装一个函数。我的实现思路是这样的:
c复制void LCD_DisplayString(uint8_t Line, uint16_t Column, char *str) {
while(*str) {
LCD_DisplayChar(Line, Column, *str++);
Column += 8; // 假设每个字符占8像素宽
}
}
这个简单的封装让我们既能保持精确的位置控制,又能方便地显示字符串。在实际使用中,你可能还需要考虑字符串长度超出屏幕宽度的情况,可以添加边界检查。
3. ADC采集与数据处理
3.1 电位器数据采集
题目中使用的是旋转电位器,通过ADC采集其电压值。电位器通常有三个引脚:两端接VCC和GND,中间引脚输出可变电压。在STM32中,我们可以这样配置ADC:
c复制void ADC_Config(void) {
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DMAContinuousRequests = DISABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK) {
Error_Handler();
}
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
Error_Handler();
}
}
3.2 数据转换与校准
在实际测试中,我发现电位器在最低位置时的电压值并非理论上的1.65V(假设VCC=3.3V),而是略低于这个值。这是正常现象,原因可能包括:
- 电位器本身的线性度误差
- ADC的参考电压偏差
- 电路中的分压影响
解决方法是为每个点位设置一个合理的范围,而不是严格的阈值。例如:
c复制#define POSITION_RANGE 50 // 每个点位对应的ADC值范围
uint8_t GetPosition(uint16_t adcValue) {
for(int i=0; i<15; i++) {
if(adcValue >= i*POSITION_RANGE && adcValue < (i+1)*POSITION_RANGE) {
return i;
}
}
return 14; // 默认返回最大值
}
4. 实时显示实现
4.1 显示更新策略
为了实现平滑的动画效果,我们需要考虑显示更新的策略。直接在每个循环中都更新显示会导致闪烁,而更新太慢又会影响实时性。我的解决方案是:
- 只在电位器位置变化时更新显示
- 更新前先清除旧的位置指示
- 使用双缓冲机制减少闪烁
具体实现如下:
c复制uint8_t lastPosition = 0;
void UpdateDisplay(uint8_t newPosition) {
if(newPosition != lastPosition) {
ClearPosition(lastPosition); // 清除旧位置
DrawPosition(newPosition); // 绘制新位置
lastPosition = newPosition;
}
}
4.2 显示效果优化
为了在LCD上呈现更好的视觉效果,我尝试了几种不同的显示方式:
- 字符箭头:使用">"、"<"等字符表示方向
- 进度条:用"["和"]"包裹中间填充的"="表示位置
- 图形指示:使用自定义字符构建更丰富的图形
最终我选择了第三种方案,因为它最能体现电位器旋转的角度变化。创建自定义字符需要先定义字模:
c复制const uint8_t customChar[8] = {
0x00, 0x00, 0x04, 0x02, 0x1F, 0x02, 0x04, 0x00
}; // 右箭头图案
然后在LCD初始化时加载这些字模:
c复制LCD_LoadCustomChar(0, customChar); // 将自定义字符存入CGRAM位置0
5. 多电位器处理
5.1 数据采集同步
题目中涉及两个电位器,需要同时采集它们的数据。这里有几个关键点需要注意:
- 采样时序:确保两个电位器的采样间隔一致
- 数据关联:正确处理两个电位器数据的逻辑关系
- 显示协调:两个电位器的显示效果不应互相干扰
我的解决方案是交替采集两个ADC通道:
c复制void ReadPots(void) {
static uint8_t channel = 0;
if(channel == 0) {
pot1Value = ReadADC(ADC_CHANNEL_1);
channel = 1;
} else {
pot2Value = ReadADC(ADC_CHANNEL_2);
channel = 0;
}
}
5.2 逻辑条件处理
题目中还提到一个特殊条件:"处于解锁状态并且角度处于1 2象限才点亮LED8"。这里的实现要点是:
- 明确"解锁状态"的判断条件
- 正确定义"1 2象限"的ADC值范围
- 处理边界条件
我的实现代码如下:
c复制void UpdateLEDs(void) {
if(isUnlocked() && (pot1Value < MIDDLE_VALUE || pot2Value < MIDDLE_VALUE)) {
LED_On(LED8);
} else {
LED_Off(LED8);
}
}
注意:在实际测试中,我发现LED点亮条件与题目描述有细微差异。这可能是因为:
- 对"1 2象限"的理解不同
- ADC值的边界处理不够精确
- 解锁状态的判断条件有误
6. 常见问题与调试技巧
6.1 LCD显示问题
问题1:显示内容闪烁
- 原因:刷新频率过高,且没有使用双缓冲
- 解决:降低刷新率,或实现双缓冲机制
问题2:字符显示位置偏移
- 原因:行号或列坐标计算错误
- 解决:检查坐标计算,注意LCD的坐标原点位置
6.2 ADC采集问题
问题1:数值跳动大
- 原因:电源噪声或采样时间不足
- 解决:
- 增加ADC采样时间
- 添加软件滤波(如移动平均)
- 检查电源稳定性
问题2:线性度差
- 原因:电位器质量或电路问题
- 解决:
- 分段校准ADC值
- 更换质量更好的电位器
- 检查分压电阻匹配
6.3 性能优化建议
- 减少不必要的显示更新:只在数据变化时更新显示
- 合理设置ADC采样率:根据需求平衡实时性和资源占用
- 使用查表法:将角度与显示位置的映射关系预先计算存储
- 优化字符串处理:避免频繁的字符串操作
7. 完整实现框架
下面给出一个完整的程序框架供参考:
c复制int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
LCD_Init();
ADC_Init();
LED_Init();
// 加载自定义字符
LoadCustomChars();
// 主循环
while (1) {
// 读取电位器值
ReadPots();
// 获取当前位置
uint8_t pos1 = GetPosition(pot1Value);
uint8_t pos2 = GetPosition(pot2Value);
// 更新显示
UpdatePositionDisplay(pos1, 0); // 第一个电位器显示在第0行
UpdatePositionDisplay(pos2, 1); // 第二个电位器显示在第1行
// 更新LED状态
UpdateLEDs();
// 适当延时
HAL_Delay(20);
}
}
8. 经验总结
通过这道题的实践,我总结了几个重要的经验:
- 精确控制胜于全局刷新:在嵌入式显示中,精确控制特定区域的更新比全屏刷新更高效
- 理解硬件特性很重要:电位器的非线性、ADC的精度限制等硬件特性会直接影响软件实现
- 实时性需要权衡:不是越快越好,要在实时性和系统负载间找到平衡点
- 调试要有方法:分段验证、添加调试输出、使用逻辑分析仪等都是有效的调试手段
这道题虽然看似简单,但真正做好需要考虑很多细节。特别是当两个电位器交互时,逻辑条件处理要格外小心。建议在实现过程中:
- 先单独测试每个功能模块
- 再逐步组合功能
- 最后优化整体性能
对于准备蓝桥杯嵌入式比赛的同学,我的建议是多练习这类综合性的题目,它们能很好地检验你对嵌入式系统各个模块的理解和整合能力。
