第4章 ADC与触摸按键:让按摩仪感知力度与触碰
各位同学,欢迎来到第四章。这一章我们要聊的是按摩仪的「感官」——ADC和触摸按键。
你想想看,一台智能按摩仪,如果不知道用户按了多大力,也不知道用户的手指放在了哪里,那它跟一块木头有什么区别?所以,这一章的内容,说白了就是给我们的设备装上「触觉」和「听觉」。
我个人习惯把ADC比作「翻译官」——它把模拟世界的电压信号,翻译成数字世界能理解的二进制数。而触摸按键呢,更像一个「侦探」,通过检测电容的变化来判断有没有手指靠近。
4.1 ADC工作原理:从电压到数字的桥梁
ADC,全称模数转换器。它的任务很简单:把一个连续的模拟电压,转换成一个离散的数字值。
为什么需要这个?因为我们的STM32芯片是数字器件,它只认识0和1。但电位器输出的电压是0V到3.3V之间的任意值,压力传感器输出的也是模拟信号。没有ADC,这些信号就进不了芯片的大脑。
ADC的核心原理,我总结为三步:
- 采样:在某个时间点,把模拟电压「冻结」住,存到一个采样电容里。
- 量化:把采样到的电压,与内部参考电压进行比较,分成若干等级。
- 编码:把量化后的等级,转换成二进制数字输出。
举个例子,一个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的做法是:
- 给触摸通道的电容充电到高电平
- 然后通过一个固定电阻放电
- 测量放电到某个阈值所需的时间
- 这个时间与电容值成正比
手指靠近时,电容变大,放电时间变长。我们检测到这个变化,就知道有人摸了。
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和触摸按键,是按摩仪感知外界的重要接口。下一章,我们会把这些感知到的数据用起来,实现真正的按摩模式控制。
记住一句话:好的固件,一半靠代码,一半靠调试。多拿示波器看看波形,比闷头写代码管用得多。