第4章 ADC与触摸按键:让按摩仪感知力度与触碰

各位同学,欢迎来到第四章。这一章我们要聊的是按摩仪的「感官」——ADC和触摸按键。

你想想看,一台智能按摩仪,如果不知道用户按了多大力,也不知道用户的手指放在了哪里,那它跟一块木头有什么区别?所以,这一章的内容,说白了就是给我们的设备装上「触觉」和「听觉」。

我个人习惯把ADC比作「翻译官」——它把模拟世界的电压信号,翻译成数字世界能理解的二进制数。而触摸按键呢,更像一个「侦探」,通过检测电容的变化来判断有没有手指靠近。

4.1 ADC工作原理:从电压到数字的桥梁

ADC,全称模数转换器。它的任务很简单:把一个连续的模拟电压,转换成一个离散的数字值。

为什么需要这个?因为我们的STM32芯片是数字器件,它只认识0和1。但电位器输出的电压是0V到3.3V之间的任意值,压力传感器输出的也是模拟信号。没有ADC,这些信号就进不了芯片的大脑。

ADC的核心原理,我总结为三步:

  1. 采样:在某个时间点,把模拟电压「冻结」住,存到一个采样电容里。
  2. 量化:把采样到的电压,与内部参考电压进行比较,分成若干等级。
  3. 编码:把量化后的等级,转换成二进制数字输出。

举个例子,一个12位的ADC,参考电压是3.3V。那么它能分辨的最小电压就是3.3V / 4096 ≈ 0.8mV。也就是说,输入电压每变化0.8mV,ADC输出的数字值就变化1。

关键公式:数字值 = (输入电压 / 参考电压) × 2^分辨率

比如输入1.65V,12位ADC:数字值 = (1.65 / 3.3) × 4096 = 2048

我在项目中遇到过一个问题:采样时间设置太短,导致采样电容没充满电,转换结果偏小。嗯,这里要注意,采样时间至少要大于采样电容的充电时间常数。

4.2 STM32的ADC配置:从理论到代码

STM32的ADC模块功能很强大,但配置起来也不复杂。我个人习惯用HAL库,因为它封装得比较友好。

配置ADC,主要做这几件事:

  • 开启ADC时钟
  • 配置ADC参数(分辨率、对齐方式、扫描模式等)
  • 配置ADC通道(哪个引脚、采样时间)
  • 校准ADC
  • 启动转换

下面是一个典型的单次转换配置代码:

// ADC初始化函数
void ADC_Init(void)
{
    ADC_HandleTypeDef hadc;
    
    __HAL_RCC_ADC1_CLK_ENABLE();  // 开启ADC1时钟
    
    hadc.Instance = ADC1;
    hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;  // 时钟分频
    hadc.Init.Resolution = ADC_RESOLUTION_12B;             // 12位分辨率
    hadc.Init.ScanConvMode = DISABLE;                     // 单通道模式
    hadc.Init.ContinuousConvMode = DISABLE;               // 单次转换
    hadc.Init.DiscontinuousConvMode = DISABLE;
    hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START;      // 软件触发
    hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;            // 右对齐
    hadc.Init.NbrOfConversion = 1;                        // 转换通道数
    
    HAL_ADC_Init(&hadc);
    
    // 配置通道
    ADC_ChannelConfTypeDef sConfig;
    sConfig.Channel = ADC_CHANNEL_0;       // PA0引脚
    sConfig.Rank = 1;
    sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;  // 采样时间
    
    HAL_ADC_ConfigChannel(&hadc, &sConfig);
    
    // 校准
    HAL_ADCEx_Calibration_Start(&hadc);
}

// 读取ADC值
uint16_t ADC_Read(void)
{
    uint16_t adc_value = 0;
    
    HAL_ADC_Start(&hadc);                    // 启动转换
    HAL_ADC_PollForConversion(&hadc, 100);   // 等待转换完成
    adc_value = HAL_ADC_GetValue(&hadc);     // 读取结果
    HAL_ADC_Stop(&hadc);                     // 停止转换
    
    return adc_value;
}

小技巧:采样时间的选择有讲究。如果信号源内阻大,采样时间要长一些。我一般用ADC_SAMPLETIME_55CYCLES_5,虽然慢一点,但数据稳定。对于电位器这种低内阻的信号,3个周期就够了。

4.3 读取电位器与压力传感器

电位器,说白了就是一个可调电阻。我们把它接成电压分压的形式,中间抽头接到ADC引脚。旋转旋钮,电压变化,ADC读数也跟着变。

压力传感器稍微复杂一点。常见的薄膜压力传感器,压力越大,电阻越小。我们需要配合一个固定电阻组成分压电路,然后读取分压点的电压。

电路连接很简单:

  • 电位器:两端接VCC和GND,中间抽头接ADC引脚
  • 压力传感器:一端接VCC,另一端通过一个10kΩ电阻接GND,中间节点接ADC引脚

读取代码和上面一样,但要注意数据处理:

// 将ADC原始值转换为电压值(单位mV)
float ADC_To_Voltage(uint16_t adc_value)
{
    return (float)adc_value * 3300.0f / 4096.0f;
}

// 将电压值转换为压力等级(0-100)
uint8_t Pressure_To_Level(float voltage)
{
    // 假设压力传感器特性:0V对应0%,3.3V对应100%
    // 实际项目中需要根据传感器标定曲线来
    uint8_t level = (uint8_t)(voltage / 33.0f);  // 33mV对应1%
    
    if(level > 100) level = 100;
    return level;
}

避坑指南:我曾经在压力传感器上吃过亏。传感器输出的电压和压力不是线性关系,而是近似反比。如果不做曲线拟合或查表,直接按线性算,结果会差很多。建议用万用表实测几个点,做个分段线性插值。

4.4 触摸按键原理:RC充放电的秘密

触摸按键,听起来很玄乎,其实原理很简单——RC充放电。

每个触摸按键本质上就是一个电容。手指靠近时,会改变这个电容的容值。我们通过测量电容的变化,就能判断有没有手指触摸。

具体怎么测?STM32的做法是:

  1. 给触摸通道的电容充电到高电平
  2. 然后通过一个固定电阻放电
  3. 测量放电到某个阈值所需的时间
  4. 这个时间与电容值成正比

手指靠近时,电容变大,放电时间变长。我们检测到这个变化,就知道有人摸了。

STM32内部有一个专门的触摸感应模块(TSC),它自动完成充放电和计时。我们只需要配置好通道,读取结果就行。

配置代码示例:

// 触摸按键初始化
void Touch_Init(void)
{
    __HAL_RCC_TSC_CLK_ENABLE();
    
    TSC_HandleTypeDef htsc;
    htsc.Instance = TSC;
    htsc.Init.CTPulseHighLength = TSC_CTPH_4CYCLES;   // 充电脉冲宽度
    htsc.Init.CTPulseLowLength = TSC_CTPL_4CYCLES;    // 放电脉冲宽度
    htsc.Init.SpreadSpectrum = DISABLE;
    htsc.Init.SynchroPinPolarity = TSC_SYNC_POLARITY_FALLING;
    htsc.Init.AcquisitionMode = TSC_ACQ_MODE_NORMAL;
    htsc.Init.MaxCountValue = 16383;                  // 最大计数值
    
    HAL_TSC_Init(&htsc);
    
    // 配置触摸通道
    TSC_ChannelConfigTypeDef channelConfig;
    channelConfig.Channel = TSC_CHANNEL_1;   // 对应某个IO口
    channelConfig.SamplingTime = TSC_SAMPLINGTIME_4CYCLES;
    channelConfig.SpreadSpectrum = DISABLE;
    
    HAL_TSC_ConfigChannel(&htsc, &channelConfig, TSC_CHANNEL_1);
}

4.5 实现触摸开关控制

有了触摸检测能力,我们就可以实现触摸开关了。逻辑很简单:

  • 检测到触摸 → 切换状态(开/关)
  • 没触摸 → 保持当前状态

但实际做起来,有几个坑要避开:

  • 抖动:触摸瞬间会有多次触发,需要做消抖
  • 阈值:不同环境下的基准值不同,需要自适应
  • 灵敏度:太灵敏容易误触,太迟钝体验差

我写了一个简单的触摸开关实现:

// 触摸开关状态
typedef enum {
    TOUCH_STATE_IDLE,      // 空闲
    TOUCH_STATE_PRESSED,   // 按下
    TOUCH_STATE_RELEASED   // 释放
} TouchState;

TouchState touch_state = TOUCH_STATE_IDLE;
uint8_t led_on = 0;

// 触摸检测与消抖
void Touch_Process(void)
{
    static uint16_t touch_count = 0;
    uint16_t touch_value = HAL_TSC_GetValue(&htsc, TSC_CHANNEL_1);
    
    // 基准值(无触摸时的值)
    static uint16_t base_value = 0;
    static uint8_t base_ready = 0;
    
    // 首次运行,获取基准值
    if(!base_ready) {
        base_value = touch_value;
        base_ready = 1;
        return;
    }
    
    // 检测触摸:当前值比基准值大一定阈值
    if(touch_value > base_value + 200) {
        touch_count++;
        if(touch_count > 5) {  // 连续5次检测到,确认触摸
            if(touch_state == TOUCH_STATE_IDLE) {
                touch_state = TOUCH_STATE_PRESSED;
                led_on = !led_on;  // 切换LED状态
                HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);
            }
        }
    } else {
        touch_count = 0;
        if(touch_state == TOUCH_STATE_PRESSED) {
            touch_state = TOUCH_STATE_RELEASED;
        } else {
            touch_state = TOUCH_STATE_IDLE;
        }
    }
}

核心要点:触摸按键的阈值不是固定的。环境温度、湿度、甚至你手上的汗都会影响电容值。我建议在设备上电时做一次自动校准,或者在运行过程中动态更新基准值。

好了,这一章的内容就到这里。ADC和触摸按键,是按摩仪感知外界的重要接口。下一章,我们会把这些感知到的数据用起来,实现真正的按摩模式控制。

记住一句话:好的固件,一半靠代码,一半靠调试。多拿示波器看看波形,比闷头写代码管用得多。