传感器模块都在交出什么:从模拟电压到数字协议

先把三种线接到同一张地图上

桌面上摆着几块常见模块:光敏、热敏和电位器。它们都能接到 STM32,但交出来的东西并不一样。先把这三种输入放到同一张地图上:

器件 模块端 参考接线 这根线交给谁
光敏电阻模块 AO PA0 / ADC1_CH0 ADC,读连续电压
光敏电阻模块 DO PB5 GPIO,读比较器后的 0/1
热敏电阻模块 AO PA1 / ADC1_CH1 ADC,读分压电压
3362P 103 中间滑动端 PA2 / ADC1_CH2 ADC,读手动电压
三个器件 GND GND 必须共地
三个器件 VCC 按产品资料供电 先确认工作电压和输出范围

光敏模块通常有 VCC、GND、AO、DO 四个引脚。AO 是传感电路产生的模拟电压,DO 是板上的比较器拿 AO 和阈值比较后的结果。模块上的小电位器,就是在改这条阈值线。

3362P 103 是 10K 电位器。把两端接到 3.3V 和 GND,中间滑动端接 PA2,它就是一只手动可调的分压器。不要把滑动端直接接到 5V 再送进 STM32 ADC。STM32F103 的 ADC 输入通常不能高于 3.3V,接线之前先用万用表量一下电压。

如果某个模块用 5V 供电,AO 可能随着光线或温度接近 5V。此时不能因为"它只是一个模拟输出"就直接插进 PA0/PA1。

ADC 没有"光照通道"

STM32F103 的 ADC 是 12 位。假设参考电压是 3.3V,它把输入电压切成 4096 个刻度:

text 复制代码
0V       -> 0
1.65V    -> 约 2048
3.3V     -> 4095

它真正做的是:采样 PA0 上的电压,转换成一个 0 到 4095 的整数。

如果光敏模块在黑暗时输出 2.4V,ADC 可能返回接近 2978;如果热敏模块在当前温度下输出 1.1V,ADC 可能返回接近 1365。数字本身没有"光"和"温度"的单位。单位来自传感器电路的关系,还需要你知道分压电阻、热敏电阻曲线、模块方向和标定结果。

c 复制代码
uint16_t light_raw = AnalogInput_ReadLight();
uint16_t thermistor_raw = AnalogInput_ReadThermistor();
uint16_t potentiometer_raw = AnalogInput_ReadPotentiometer();

这三个变量的名字已经在提醒我们:它们是 raw,是原始采样值。它们还不是 temperature 或 lux。

光敏模块:AO 和 DO 同时接出来,差别才会露出来

先只做一个动作:用手遮住光敏电阻,再拿手机灯照它。OLED 第一行显示 AO,第四行显示 DO。同时拧光敏模块上的电位器,让 DO 的状态翻转。

程序里,AO 走 ADC,DO 走普通 GPIO:

c 复制代码
static uint16_t AnalogInput_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);
}

uint16_t AnalogInput_ReadLight(void)
{
    return AnalogInput_ReadChannel(ADC_Channel_0);
}

uint8_t AnalogInput_IsLightThresholdActive(void)
{
    uint8_t level;

    level = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_5) == Bit_SET;

    /* 常见模块是低电平触发,换模块时只改这一层。 */
    return (uint8_t)!level;
}

这里的 !level 不是玄学。它只是把"引脚读到低电平"翻译成"阈值被触发"。如果你手上的模块恰好高电平触发,改这一层就够了,不要让上层业务到处出现 if (GPIO_Read... == Bit_RESET)。

DO 的状态由模块电位器决定。拧动电位器,改变的是比较器的阈值,不是光敏电阻本身突然变灵敏。AO 曲线仍然是传感器原始电压;DO 只是告诉你"这次有没有越过阈值"。

热敏模块:先看到电压,再谈温度

常见热敏模块把热敏电阻和固定电阻组成分压。温度改变,热敏电阻阻值改变,分压点电压跟着改变,ADC 读到的就是这个分压点。

text 复制代码
3.3V
 |
[热敏电阻]
 |
 +------> AO ------> PA1 / ADC1_CH1
 |
[固定电阻]
 |
GND

手指按住热敏电阻,数字可能变化得很慢。手指的热量先传到封装,再改变阻值,分压电压才移动;松开以后,它又慢慢回到环境温度。用 OLED 连续看几次读数,就能看到这个变化过程。至于精确的摄氏度,还需要完整的参数和标定数据。

如果要把 ADC 数值换成温度,至少还要知道:热敏电阻的标称阻值、B 值或完整阻值曲线、分压电阻值、供电电压,以及模块的实际误差。少一个条件,公式都可能只是漂亮的错数。

3362P:最诚实的模拟输入

电位器没有"传感器算法"。你拧多少,它就把滑动端电压推到哪里。它最适合用来排查 ADC:

  • 数字从接近 0 连续走到接近 4095,说明 ADC 基本链路在工作;
  • 数字卡在某个范围,先量滑动端电压,再查供电、接地和引脚;
  • 数字跳动明显,先检查面包板接触、供电和采样线;
  • 模块上的调节电位器改变 DO 翻转点,本质上也是在改变一个比较电压。
c 复制代码
uint16_t potentiometer_raw = AnalogInput_ReadPotentiometer();

OLED_ShowNum(0, 16, potentiometer_raw, 4, OLED_6X8);

把电位器从一端慢慢拧到另一端,OLED 上的数字应该跟着连续变化。它给 ADC 提供了一份能由手动控制、能重复回到原点的输入。

一次采样,代码里到底准备了什么

初始化函数把三路 ADC 配成模拟输入,把光敏 DO 配成上拉输入,再完成 ADC 时钟和校准:

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

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

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

    gpio.GPIO_Pin = GPIO_Pin_5;
    gpio.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &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))
    {
    }
}

GPIO_Mode_AIN 是在告诉 GPIO:这三根脚不再负责输出高低电平,而是把外部电压交给 ADC。ADC_RegularChannelConfig() 每次选择当前要读的通道,软件触发一次转换,EOC 置位后才取结果。

三路输入每 10ms 读一次,足够观察光敏、热敏和手动电位器。每次只选择一个通道、启动一次转换、等待完成,再读取结果,过程清清楚楚。

主循环按固定间隔采样,并把结果显示在屏幕上:

c 复制代码
if (Timer_Expired(&sample_time, 10))
{
    light_raw = AnalogInput_ReadLight();
    thermistor_raw = AnalogInput_ReadThermistor();
    potentiometer_raw = AnalogInput_ReadPotentiometer();
    light_threshold_active = AnalogInput_IsLightThresholdActive();
}

if (Timer_Expired(&oled_time, 100))
{
    OLED_ShowNum(0, 0, light_raw, 4, OLED_6X8);
    OLED_ShowNum(0, 8, thermistor_raw, 4, OLED_6X8);
    OLED_ShowNum(0, 16, potentiometer_raw, 4, OLED_6X8);
    OLED_ShowNum(0, 24, light_threshold_active, 1, OLED_6X8);
}

DO 是离散状态,直接显示 0/1 就足够;AO 则显示 ADC 的原始数值。这样看屏幕时,可以同时对照连续变化的电压和越过阈值后的状态。

看到异常,先检查输入

光敏 AO 一直不变

先用万用表测 AO 对 GND 的电压,再拧模块电位器。电位器通常只影响 DO 阈值,不能保证 AO 一定大幅变化;如果 AO 本身接近固定电压,检查光敏元件、供电和模块方向。

DO 一直是 0 或一直是 1

看模块指示灯,再慢慢拧阈值电位器。确认 PB5 的有效电平定义,必要时只改 LIGHT_DO_ACTIVE_LOW。不要把 DO 的状态异常直接归咎于 ADC,因为 DO 根本没有经过 ADC。

热敏数值变化很小

这可能是热量传递慢,也可能是模块分压范围窄。把手指按住几十秒,再松开观察曲线;如果只看一瞬间的 OLED 数字,很容易把"响应慢"误判成"代码没工作"。

电位器不能走满 0 到 4095

先量两端是否真的接在 3.3V 和 GND,滑动端是否接对。电位器阻值、ADC 输入范围、供电和接触电阻都会影响端点;不要为了追求满量程把 ADC 输入接到超过 3.3V 的电压。

这几种输入为什么不能共用一个"传感器函数"

它们最后都可以得到数字,但数字之前发生的事情不同:

text 复制代码
光敏 AO       -> 电压 -> ADC 原始值
光敏 DO       -> 比较器阈值 -> GPIO 状态
热敏电阻 AO   -> 分压电压 -> ADC 原始值 -> 需要曲线才能换温度
3362P         -> 手动分压电压 -> ADC 原始值

如果接口只写成 Sensor_Read(),调用者很快就会忘记返回的是电平、ADC 原始值,还是已经换算过的物理量。把名字写具体一些:ReadLight()、ReadThermistor()、ReadPotentiometer(),并注明它们当前返回的仍然是原始 ADC 数字。

先把这几种输入的来路分清楚:传感器给出电压,ADC 把电压变成数字,模块上的比较器则把连续变化压成一个阈值状态。未经标定的热敏模块,不能只凭一个 ADC 数字得出可靠的摄氏度。

ADC 不是"光照值"和"温度值"的翻译器。它只是电压的记账员;至于这笔电压账最后代表什么,要由传感器电路、标定数据和你的工程目标共同决定。

先让 DATA 线在空闲时站稳

裸 DHT11 有四个针脚,实际接三根。NC 保持悬空。不同批次和封装的脚位顺序可能不同,接线前先对照自己手上的资料和元件正面标记。

DHT11 引脚 参考接法 观察什么
VCC 3.3V;也可按资料使用 5V 先确认 DATA 高电平会不会超过 MCU 可承受范围
DATA PB2 主机发起读取,DHT11 回传数据
NC 不接 没有功能
GND STM32 GND DHT11 和开发板共地

DATA 和 VCC 之间需要一颗上拉电阻 R1,常用 4.7K 到 10K。很多 DHT11 模块已经把它焊在板上,裸传感器则需要外接。接线前先看清模块原理图或板上丝印:已经有 R1 的模块,不再重复加一颗。

text 复制代码
VCC
 |
[R1]
 |
 +------ DATA ------ PB2
 |
DHT11
 |
GND

DHT11 需要发低电平时,把 DATA 拉到地;它松手时,R1 把线带回高电平。STM32 也遵守这个习惯:发起读取时把 PB2 拉低,随后把 PB2 改成输入,把这根线还给 DHT11。

没有 R1,DATA 在双方都松手时就没有明确的高低电平,程序读到的结果也不可靠。R1 的工作不是制造一串数据,而是给这根共享的数据线准备稳定的空闲状态。

DATA 上跑的是 40 个 bit

DHT11 每次回传 5 个字节:

text 复制代码
湿度整数 | 湿度小数 | 温度整数 | 温度小数 | 校验和

DHT11 常见整数型号的小数字节多为 0。第五个字节由前四个相加得到:

c 复制代码
checksum = humidity_integer + humidity_decimal +
           temperature_integer + temperature_decimal;

这里没有一个"2.1V 等于 26 度"的换算公式。DATA 线上先后经过的是一段段低电平和高电平。每一位先低约 50us;后面的高电平很短时记为 0,持续约 70us 时记为 1。

text 复制代码
低约 50us | 高约 26~28us  -> 0
低约 50us | 高约 70us     -> 1

主机还要先敲门:

text 复制代码
STM32 拉低 DATA 至少 18ms
STM32 释放 DATA
DHT11 拉低约 80us,再拉高约 80us
DHT11 连续发送 40 位

观察 PB2 的程序状态和 DHT11 返回结果时,最该先找的不是一整帧数据。先分清主机拉低的起始动作,再分清代表 0 和 1 的两种高电平时长。

PB2 在一次读取中换了两次身份

一次读取里,PB2 要先做输出,再做输入:

c 复制代码
static void DHT11_SetOutput(void)
{
    GPIO_InitTypeDef gpio;

    gpio.GPIO_Pin = GPIO_Pin_2;
    gpio.GPIO_Mode = GPIO_Mode_Out_OD;
    gpio.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &gpio);
}

static void DHT11_SetInput(void)
{
    GPIO_InitTypeDef gpio;

    gpio.GPIO_Pin = GPIO_Pin_2;
    gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOB, &gpio);
}

开始读取时,STM32 把 PB2 配成开漏输出,拉低 DATA。协议要求这段低电平至少 18ms,而这里用的是空循环延时,不是精确的毫秒计时------它一个"单位"大约 2µs 上下,随主频和编译器变,所以 19000 落地时比 19ms 要长,接板子前先按自己的时钟校准一次:

c 复制代码
DHT11_SetOutput();
GPIO_ResetBits(DHT11_PORT, DHT11_PIN);
DHT11_DelayUs(19000);
GPIO_SetBits(DHT11_PORT, DHT11_PIN);
DHT11_DelayUs(30);
DHT11_SetInput();

最后一行一执行,STM32 就不再控制 DATA。随后等 DHT11 先拉低、再拉高。等不到预期电平,DHT11_WaitLevel() 返回,DHT11_Read() 给出 DHT11_TIMEOUT。没有超时保护的版本会卡在 while 里,线路没有响应时程序就再也回不到主循环。

每一位高电平开始后,程序等一小段再看一次 PB2。这里的 40 同样受空循环延时的影响,不是精确的 40µs(按上面那个单位估算落在 80~90µs 这一档)------它的作用是在高电平结束前采一次样,所以这个数要按自己的板子校准,别照抄:

c 复制代码
DHT11_DelayUs(40);
data <<= 1;

if (DHT11_ReadLevel())
{
    data |= 1;
}

短高电平已经结束,读到 0;长高电平还在,读到 1。这就是为什么 DATA 接在普通 GPIO 上,驱动里却反复谈时间。ADC 擅长把稳定电压转换成数字;DHT11 需要程序分辨脉冲什么时候落下。

两秒读一次就够了

DHT11 自己完成一次测量需要时间,常见资料给出的两次读取间隔至少约 1 秒。每两秒请求一次,能留出明显余量。屏幕可以继续显示上一次成功读到的温度、湿度和状态码;到了下一次读取时间,再取一组新数据。

一次读取本身会占用几十毫秒,因为程序必须等完那 40 位高低电平。间隔的两秒并不是程序停住两秒,而是等到下一次该读取时再发起请求。

温湿度数字先要过三道关

屏幕上显示温度和湿度之前,驱动已经做了三次判断:DHT11 有没有回应,40 个 bit 有没有按时读完,最后的校验和能不能对上。结果可以归成三种状态:

text 复制代码
0:收到完整数据,校验通过
1:等响应或等位超时
2:收满五个字节,校验失败

0 表示五个字节都收到了,而且第五个字节与前四个字节相加的结果一致,这时温湿度才可以更新到屏幕上。

1 表示程序在等某个该出现的电平,却一直没有等到。DATA 没接好、上拉电阻缺失、供电异常、读取间隔太短,都可能走到这个结果。它的意义是"这一帧没有读完",不是"当前温湿度为 1"。

2 表示 40 个 bit 已经全部收到,但最后一项核对没有通过。这些 bit 有可能在传输中受干扰,也有可能其中某一位的时长被误判。驱动不需要猜是哪一位出了问题,只需要拒绝用这帧数据覆盖上一次正确的温湿度。

这三种状态比"屏幕上有个 26 度"更早告诉你程序走到了哪里。上拉电阻保证空闲电平,超时判断防止程序永远等待,校验和负责拦住看似完整却不可信的数据。

DHT11 给 STM32 的是一帧有时序的数据。PA0 上的光敏 AO 可以交给 ADC;PB2 上的 DHT11 DATA 需要按脉冲宽度读取。线只有一根,里面跑的东西完全不同。

先把"看哪里"摆在桌面上

模块 观察对象 触发动作 典型用途
单个 TCRT5000 / 单路灰度 一处近距离的反射 黑白或距离变化让通道翻转 到位检测、近物检测
八路循迹 / 八路灰度 车底八处位置的反射差异 黑线让若干通道翻转 判断线在左还是右
对射红外 发射端和接收端之间的光路 中间出现遮挡物时翻转 计数、门边检测、掉落检测
光敏模块 DO 当前位置的环境光 光线越过阈值时翻转 光暗开关

单个 TCRT5000 和八路循迹模块本质上是同一种反射式传感器,八路循迹模块也常被叫作灰度传感器。前者把一组发射管和接收管对着一小块区域,后者把多组相同的器件排成一行。把八路模块拆开看,其中任意一路和单个 TCRT5000 都是在交出一个反射状态;确认供电和输出电平后,两者可以按同一种 GPIO 输入方式接入。真正不同的是数量和安装位置:一个点只能回答"眼前这一小块区域是什么样",八个点还能比较出黑线偏左还是偏右。

对射不看目标反射回来的光。发射端一直对着接收端,接收端只等那一束光有没有到达。手指伸进中间,状态就变;手指拿走,状态又恢复。

把状态接到看得见的地方

下面这组引脚用于把这些状态接到同一块开发板上:

信号 参考引脚 有效状态
对射 DO PB0 低电平表示光路被挡
TCRT5000 DO PB2 低电平表示近处反射触发
光敏 DO PB5 低电平表示越过光照阈值
有源蜂鸣器 IN PB12 低电平响
外接 LED PC13 TCRT 有效时点亮

模块的 VCC、GND 按自己买到的产品资料接,三个 DO 和 STM32 必须共地。对射模块的发射端和接收端也要分别供电,先让两端指示灯稳定,再把 DO 接 PB0。

底层接口只保留"这个状态是否有效":

c 复制代码
static uint8_t DigitalSensor_ReadPin(uint16_t pin,
                                     uint8_t active_low)
{
    uint8_t level;

    level = GPIO_ReadInputDataBit(GPIOB, pin) == Bit_SET;

    if (active_low)
    {
        return (uint8_t)!level;
    }

    return level;
}

换一块输出逻辑相反的模块,只改 BEAM_ACTIVE_LOW 或 REFLECT_ACTIVE_LOW,屏幕、蜂鸣器和 LED 的逻辑不用跟着散开。

第一次运行:保持初始状态

上电后让对射两端正对,TCRT5000 前方放一块距离合适的白纸,光敏模块放在桌面上。OLED 三行显示:

text 复制代码
beam    reflect    light

三行的具体 0/1 取决于模块的有效电平定义。先看模块指示灯和代码里的有效电平宏,把实际状态记录下来。

程序每 10ms 读取一次 GPIO:

c 复制代码
if (Timer_Expired(&sample_time, 10))
{
    beam_blocked = DigitalSensor_ReadBeam();
    reflect_active = DigitalSensor_ReadReflect();
    light_active = DigitalSensor_ReadLight();
}

10ms 对手动遮挡和近物检测足够。需要快速计数时,采样频率还要按目标运动速度重新安排;三种"1"背后,对应的是三种不同的空间关系。

让两个动作把区别说出来

c 复制代码
Buzzer_Set(beam_blocked);

if (reflect_active)
{
    LED_On();
}
else
{
    LED_Off();
}

PB12 接常见三针有源蜂鸣器模块,触发端低电平时发声。这里把它当成"低电平进去就响"的黑盒;有源和无源的发声原理另开文章。

把手指插进对射两端之间。蜂鸣器应该随着 beam_blocked 的有效状态动作;拿开手指,声音停止。把白纸移到 TCRT5000 前面,PC13 外接 LED 随 reflect_active 翻转;纸离开,LED 恢复。

对射的手指不需要靠近接收头,只要穿过中间那条线;TCRT5000 的手指必须进入近距离反射范围;光敏没有遮挡动作也可以触发,只要环境亮暗改变。

让状态变化看得见

如果每 10ms 都刷新一次屏幕,重复的 0 和 1 没有太大意义。可以只在状态发生变化时更新显示:

c 复制代码
if (beam_blocked != old_beam)
{
    old_beam = beam_blocked;
    OLED_ShowNum(0, 0, beam_blocked, 1, OLED_6X8);
}

if (reflect_active != old_reflect)
{
    old_reflect = reflect_active;
    OLED_ShowNum(0, 8, reflect_active, 1, OLED_6X8);
}

用纸片连续遮挡对射三次,屏幕上的 beam 应该跟着变化;把纸片在 TCRT5000 前后移动,reflect 的变化对应的是距离和表面反射,不是某一条固定光路被截断。

光敏 DO 的变化和手机灯、手掌阴影、环境灯有关;光敏 AO 和 DO 的阈值关系,可以对照前文的实验观察。

三个实验,把"红外"拆开

实验一:对射只认光路

先让发射端和接收端正对,记录 beam 的初始值。用硬纸片从两端之间穿过,保持纸片不碰模块;再把纸片移走。

记录三件事:纸片没有进入光路时的状态、挡住光路时的状态和蜂鸣器反应、移走后多久恢复。把纸片放在发射端旁边、接收端旁边,但不挡中间直线,状态不应该因为"附近有一块纸"就改变。

实验二:TCRT5000 需要一个反射面

先把白纸放在模块前方,慢慢拉远,记录 LED 什么时候熄灭。换成黑纸重复,再换一个有光泽的物体。

不要求不同模块给出同一个距离。留下自己的三条记录:白纸触发范围、黑纸触发范围、亮面物体的反应。TCRT5000 看的不是抽象的"有没有东西",它看的是返回到接收管的反射光够不够越过阈值。

把手指放在模块侧面,既不挡正前方,也不让反射面进入感应区。LED 如果仍翻转,调整安装角度和遮光条件,检查环境光是不是从侧面进来了。

实验三:光敏 DO 不等于 TCRT5000 DO

把手机灯照向光敏模块,再用手掌遮住;同时保持 TCRT5000 前面的白纸位置不变。记录三行 OLED 的变化。

然后把手机灯移走,拿白纸在 TCRT5000 前后移动,观察哪一行变化。两种动作可以分别触发两个状态,说明 DO 只描述输出形式,没有说明传感器正在观察什么。

故障时先看空间关系

对射一直显示"被挡"时,让两端靠近、正对,观察接收端指示灯;如果光路这样仍不恢复,检查 VCC、GND 和发射/接收端。再把两端一点点拉开,找出稳定距离。

TCRT5000 一直触发时,把目标拿远,再拧模块电位器;一直不触发时,把白纸贴近,确认发射灯在工作。黑纸、白纸和亮面材料的结果不同,属于反射条件变化。

光敏 DO 一直翻转时,重新观察 AO 电压和阈值电位器;两者的变化能帮助定位问题。

只有当指示灯、供电和实际动作都正常,OLED 上的 0/1 方向却反了,才改 ACTIVE_LOW。

八路循迹和 TCRT5000 都是在比较反射光,只是观察点数量不同;对射红外则把发射端和接收端分开,判断的是中间光路是否被挡住。把八路模块中的一路单独使用时,接线和读取方式可以与单个 TCRT5000 按同一思路处理,再根据实际输出电平调整有效状态。

传感器只交出状态。对射状态让蜂鸣器响,TCRT5000 状态让 LED 亮;上层动作改变时,底层读取函数仍然可以不动。

最后橘猫说

传感器模块到底在交出什么,不外乎三种:模拟电压、比较器切好的 0/1、以及按节奏送出来的数字协议。AO 的电压要交给 ADC,DO 的电平交给 GPIO,DHT11 的 40 个 bit 要在一根数据线上慢慢收。先把一根线到底交出什么问清楚,接错接口、读错数值的弯路能少走一大半。十分推荐大家去看看 ADC 读数的原理,十分的天才。

最后蜘蛛说

传感器天天把风吹草动整理成小报告递给我,我对着屏幕看了半天,最后发现是杜邦线在和我玩捉迷藏。

相关推荐
程序员zgh2 小时前
C++ erase() 函数用法
c语言·开发语言·c++·学习
FakeOccupational2 小时前
【电路笔记 STM32】Cortex-M7 UART 中断模式(串口接收中断+串口空闲中断)
笔记·stm32·单片机
小李不想当小白2 小时前
USART串口协议(STM32标准库学习笔记)
经验分享·笔记·stm32·单片机·嵌入式硬件·学习
无垠的广袤2 小时前
拆解电子价签
单片机·蓝牙·diy·调试器·jlink·墨水屏
迪丽热爱3 小时前
一、C语言概述与程序结构
c语言·开发语言·算法
小何好运暴富开心幸福3 小时前
嵌入式历史与生态
stm32·单片机·mcu
甜甜的大香瓜4 小时前
【MT32F006】MT32F006之Stimer延时(轮询)
单片机
辰哥单片机设计4 小时前
MP3播放器模块(STM32)
stm32·单片机·嵌入式硬件
Brilliantwxx5 小时前
【STM32】 工程实战 WS2812B 全彩 LED 灯控制(DMA 驱动)
stm32·单片机·嵌入式硬件·mcu