把 AO 接回来:黑线和白底之间到底差了多少

经典三问

Q: 传感器的 AO 是什么?

A: AO 是与被测量连续变化相关的模拟电压,STM32 通过 ADC 把它转换成数字量。

Q: 为什么要观察 AO 原始值?

A: 原始值能显示黑白差异、饱和和噪声,帮助决定阈值和安装高度,而不是盲目调控制参数。

Q: AO 常见使用场景?

A: 反射强度测量、光照采样、温度分压和需要软件自行设定阈值的传感器输入。


上一节的 DO 只会回答"压线了"或"没压线"。它让小车很快跑起来,也把一个问题藏了起来:模块做出这个 0/1 判断之前,传感头到底看见了什么?

循迹模块上的 AO,就是 Analog Output,模拟输出。红外发射管照向地面,接收端收到的反射光会随着黑线、白底、距离和高度改变;模块先把这种变化变成一个电压,再从 AO 引脚交出来。它不是只有 0 和 1,而是可能停在 0.4V、1.6V 或 2.8V 这样的中间位置。

普通 GPIO 读取高低电平时,只会把引脚理解成 0 或 1,读不出"现在是 1.6V"。ADC 是 STM32 内部专门读取电压的外设。它会取一次 AO 的电压,把 0V 到参考电压之间的位置换成一个整数;在 F103 上,这个整数是 0 到 4095。这样,程序才第一次看见比较器做出判断之前的原始变化。

这一次先做两件事:把 AO 接进 ADC,读出两路原始值;再让黑线从左扫到右,看左右数字究竟怎样变化。

两路就够把差别看出来

先不把 8 路 AO 全接 ADC。STM32F103C8T6 的 ADC 通道有限;八路同时接入会把采样顺序、引脚分配和干扰问题叠在一起。先看左右两路,黑线和白底的差别已经足够清楚。

先选车头左右两个模块:

text 复制代码
左模块 AO -> PA1 / ADC1_CH1
右模块 AO -> PA2 / ADC1_CH2
两个模块 GND -> STM32 GND

模块的 VCC 可以按它的产品资料使用 3.3V 或 5V。

这也是 AO 和 DO 的区别之一。DO 常常只有"能不能被识别为高低电平"的兼容问题;AO 必须关心它具体是多少伏。

12 位 ADC 并不会替你创造灰度

F103 的 ADC 把 0 到参考电压之间的电压分成 4096 份,结果是 0 到 4095:

text 复制代码
0V          -> 0
接近 3.3V   -> 接近 4095

它没有认识黑线、白底、循迹和左右偏移。它只是每次拿到 AO 电压,切成一个整数。AO 本身已经饱和,ADC 就会非常诚实地把饱和的结果报成 4095;AO 在白底上只落在较低区域,ADC 也只会报出一串较小数字。

下面的初始化把 PA1、PA2 配成模拟输入,并把 ADC1 时钟从 72MHz 的 PCLK2 除到 12MHz:

c 复制代码
void LineAo_Init(void)
{
    GPIO_InitTypeDef gpio;
    ADC_InitTypeDef adc;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
                           RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

    gpio.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
    gpio.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &gpio);

    adc.ADC_Mode = ADC_Mode_Independent;
    adc.ADC_ScanConvMode = DISABLE;
    adc.ADC_ContinuousConvMode = DISABLE;
    adc.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    adc.ADC_DataAlign = ADC_DataAlign_Right;
    adc.ADC_NbrOfChannel = 1;
    ADC_Init(ADC1, &adc);

    ADC_Cmd(ADC1, ENABLE);
    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1))
    {
    }
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1))
    {
    }
}

ADC 时钟不能因为 CPU 是 72MHz 就直接跟着跑 72MHz。标准库中的 RCC_ADCCLKConfig(RCC_PCLK2_Div6) 把它设成 12MHz,处在 F1 ADC 允许范围内。校准也不是装饰:它让 ADC 用芯片内部的校准过程修正自身偏差,第一次做模拟采样时不要跳过。

读取一条 AO,发生的事情没有那么神秘

c 复制代码
static uint16_t LineAo_ReadChannel(uint8_t channel)
{
    ADC_RegularChannelConfig(ADC1, channel, 1,
                             ADC_SampleTime_55Cycles5);
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);

    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET)
    {
    }

    return ADC_GetConversionValue(ADC1);
}

这几句分别做了四件事:选定本次读取哪一路 ADC 通道;设置采样时间;软件触发一次转换;等 EOC 这个"转换完成"标志出现,再取走 0 到 4095 的数字。

ADC_SampleTime_55Cycles5 不等于"转换花了 55.5ms"。它是 ADC 时钟周期数。采样时间太短时,传感器输出、电阻分压或长导线未必能及时给 ADC 内部采样电容充到真实电压,读数会偏;这里使用相对宽松的时间,不拿最快速度赌稳定性。

左右模块只是分别调用:

c 复制代码
left_raw = LineAo_ReadLeft();
right_raw = LineAo_ReadRight();

这里每次只读一路:启动转换,等结果出来,再读下一路。左右两路各读一次,已经足够看见黑线经过时的变化。

这两个函数做的事只是把通道号定下来,各调一次上面那个读取函数:

c 复制代码
uint16_t LineAo_ReadLeft(void)
{
    return LineAo_ReadChannel(ADC_Channel_1);
}

uint16_t LineAo_ReadRight(void)
{
    return LineAo_ReadChannel(ADC_Channel_2);
}

先把两路数字放到 OLED 上

前面的 OLED 已经能显示数字,这里直接让第一、二行显示左右两路 AO 原始值:

c 复制代码
OLED_ShowString(0, 0, "L:", OLED_8X16);
OLED_ShowNum(16, 0, left_raw, 4, OLED_8X16);
OLED_ShowString(0, 24, "R:", OLED_8X16);
OLED_ShowNum(16, 24, right_raw, 4, OLED_8X16);
OLED_Update();

left_raw 和 right_raw 都是 ADC 给出的原始整数。屏幕显示 0032、0486、4095 时,前面的 0 只是为了让数字位置不乱;真正需要看的是它们随黑线移动而发生的变化。

把黑线从左扫到右,不要只盯一个数字

OLED 第一行显示左路原始值,第二行显示右路原始值。把黑线慢慢从左模块下方移动到右模块下方。

理想情况下,左路数字会先变化,右路数字随后变化。若模块的输出已经接近饱和,屏幕上的数值可能更像这样:

text 复制代码
白底和灰色边缘:大多数低于 500
压到黑线:迅速冲到接近 4095

这组数据至少能说明三件事:

  1. AO 的方向和 DO 的方向是否与你的模块一致。
  2. 两个模块的"低区间"和饱和点是否相同。
  3. 黑线经过时,变化是慢慢爬坡还是像开关一样跳变。

若读数确实几乎只有低区和满量程,就把这个结果记成"低区加饱和"。它说明模块交给 ADC 的电压并没有均匀铺满 0 到 4095,而是集中在少数几个区域;连续观察一段时间,比只看某一刻的单个数字更容易发现这一点。

最后橘猫说

把模块的 DO 和 AO 同时接出来做一次对照。AO 电压越过某个区间时,DO 会从 0 翻到 1;拧模块电位器,DO 的翻转位置改变,AO 的原始读数却不会因为比较器阈值而改变。

这正是两者的分工:

text 复制代码
DO:模块替你决定"现在算黑还是白"
AO:把决定前的电压交给你,黑白判断的界线由程序自己决定

环境光改变、模块高度变化、白底反光、供电波动、两只同型号模块的器件差异,在 AO 原始读数里更容易被看见。DO 会把这些变化压缩成"还没翻转"或"已经翻转",简单得多,也失去了一部分排错线索。

AO 不会替模块凭空增加细节,它只把比较器之前的电压交给 STM32。两路数值是不是一起变化、黑线经过时会不会突然跳到满量程、阈值附近会不会抖动,都要从这组原始数据里看出来。

相关推荐
桀人3 小时前
USART帧格式
网络·stm32·单片机·嵌入式硬件
深圳老胡3 小时前
STM32F4 OTA 升级双 App 方案设计与实现
笔记·stm32·单片机·代码规范
2401_888859715 小时前
STM32H733 MPU、AXI、FMC学习
java·开发语言·stm32·spring
深圳老胡5 小时前
STM32F4 OTA升级:单App与双App方案优缺点对比
笔记·stm32·单片机·代码规范
BSD_CGQ15 小时前
FSR传感器STM32 DMA高速采样工程实现(高精度、低CPU占用)
stm32·单片机·嵌入式硬件·压力传感器·源头工厂·柔性薄膜压力传感器·可变电阻压力传感器
骑着毛驴数星星18 小时前
STM32 CAN波特率与采样点计算,基于HAL库
stm32·单片机·嵌入式硬件
新晨单片机设计21 小时前
S023A-基于STM32单片机智能插座【Proteus仿真+Keil程序+原理图】
stm32·单片机·proteus·单片机智能插座
qq_4017004121 小时前
STM32 GPIO 精讲|推挽 / 开漏 / 上拉浮空,工程选型避坑
stm32·单片机·嵌入式硬件
创界工坊工作室1 天前
19-基于单片机的宠物喂养控制系统设计
stm32·单片机·嵌入式硬件·proteus·宠物