上一章,我們介紹了 STM32F1 的輸入捕獲功能及其使用。這一章,我們將向大家介
紹如何通過輸入捕獲功能,來做一個電容觸摸按鍵。在本章中,我們將用 TIM5 的通
道 2(PA1)來做輸入捕獲,並實現一個簡單的電容觸摸按鍵,通過該按鍵控制
DS1 的亮滅。從本章分為如下幾個部分:
16.1 電容觸摸按鍵簡介
16.2 硬體設計
16.3 軟體設計
16.4 下載驗證
16.1 電容觸摸按鍵簡介
觸摸按鍵相對於傳統的機械按鍵有壽命長、佔用空間少、易於操作等諸多優點。大家
看看如今的手機,觸控螢幕、觸摸按鍵大行其道,而傳統的機械按鍵,正在逐步從手機
上面消失。本章,我們將給大家介紹一種簡單的觸摸按鍵:電容式觸摸按鍵。
我們將利用戰艦 STM32F103 上的觸摸按鍵(TPAD),來實現對 DS1 的亮滅控
制。這裡 TPAD其實就是戰艦 STM32F103 上的一小塊覆銅區域,實現原理如圖
16.1.1 所示:
這裡我們使用的是檢測電容充放電時間的方法來判斷是否有觸摸,圖中 R 是外接的電
容充電電阻,Cs 是沒有觸摸按下時 TPAD 與 PCB 之間的雜散電容。而 Cx 則是有手
指按下的時候,手指與 TPAD 之間形成的電容。圖中的開關是電容放電開關(由實
際使用時,由 STM32F1 的IO 代替)。
先用開關將 Cs(或 Cs+Cx)上的電放盡,然後斷開開關,讓 R 給 Cs(或 Cs+Cx)
充電,當沒有手指觸摸的時候,Cs 的充電曲線如圖中的 A 曲線。而當有手指觸摸的
時候,手指和 TPAD之間引入了新的電容 Cx,此時 Cs+Cx 的充電曲線如圖中的 B
曲線。從上圖可以看出,A、B兩種情況下,Vc 達到 Vth 的時間分別為 Tcs 和
Tcs+Tcx。
其中,除了 Cs 和 Cx 我們需要計算,其他都是已知的,根據電容充放電公式:
Vc=V0*(1-e^(-t/RC))
其中 Vc 為電容電壓,V0 為充電電壓,R 為充電電阻,C 為電容容值,e 為自然底
數,t 為充電時間。根據這個公式,我們就可以計算出 Cs 和 Cx。利用這個公式,我
們還可以把戰艦開發板作為一個簡單的電容計,直接可以測電容容量了,有興趣的朋
友可以搗鼓下。
在本章中,其實我們只要能夠區分 Tcs 和 Tcs+Tcx,就已經可以實現觸摸檢測了,當
充電時間在 Tcs 附近,就可以認為沒有觸摸,而當充電時間大於 Tcs+Tx 時,就認為
有觸摸按下(Tx為檢測閥值)。
本章,我們使用 PA1(TIM5_CH2)來檢測 TPAD 是否有觸摸,在每次檢測之前,我們
先配置PA1 為推輓輸出,將電容 Cs(或 Cs+Cx)放電,然後配置 PA1 為浮空輸
入,利用外部上拉電阻給電容 Cs(Cs+Cx)充電,同時開啟 TIM5_CH2 的輸入捕獲,
檢測上升沿,當檢測到上升沿的時候,就認為電容充電完成了,完成一次捕獲檢測。
在 MCU 每次復位重啟的時候,我們執行一次捕獲檢測(可以認為沒觸摸),記錄此
時的值,記為 tpad_default_val,作為判斷的依據。在後續的捕獲檢測,我們就通過
與 tpad_default_val 的對比,來判斷是不是有觸摸發生。
關於輸入捕獲的配置,在上一章我們已經有詳細介紹了,這裡我們就不再介紹。至
此,電容觸摸按鍵的原理介紹完畢。
16.2 硬體設計
本實驗用到的硬體資源有:
1) 指示燈 DS0 和 DS1
2) 定時器 TIM2
3) 觸摸按鍵 TPAD
前面兩個之前均有介紹,我們需要通過 TIM5_CH2(PA1)採集 TPAD 的信號,所以
本實驗需要用跳線帽短接多功能埠(P10)的 TPAD 和 ADC,以實現 TPAD 連接
到 PA1。如圖 16.2.1所示:
硬體設置(用跳線帽短接多功能埠的 ADC 和 TPAD 即可)好之後,下面我們開始
軟體設計。
16.3 軟體設計
軟體設計我們在之前的工程上面增加,由於本章我們用不到 timer.c,所以先刪掉
timer.c。然後在 HARDWARE 文件夾下新建 TPAD 的文件夾。然後打開 USER 文件
夾下的工程,新建一個 tpad.c 的文件和 tpad.h 的頭文件,保存在 TAPD 文件夾
下,並將 TPAD 文件夾加入頭文件包含路徑。
我們在 tpad.c 裡輸入如下代碼:
#include "tpad.h"
#include "delay.h"
#include "usart.h"
TIM_HandleTypeDef TIM5_Handler; //定時器 5 句柄
#define TPAD_ARR_MAX_VAL 0XFFFF //最大的 ARR 值(TIM5 是 16 位定時器)
vu16 tpad_default_val=0; //空載的時候(沒有手按下),計數器需要的時間
//初始化觸摸按鍵
//獲得空載的時候觸摸按鍵的取值.
//psc:分頻係數,越小,靈敏度越高.
//返回值:0,初始化成功;1,初始化失敗
u8 TPAD_Init(u8 psc)
{
u16 buf[10];
u16 temp;
u8 j,i;
TIM5_CH2_Cap_Init(TPAD_ARR_MAX_VAL,psc-1);//設置分頻係數
for(i=0;i<10;i++)//連續讀取 10 次
{
buf[i]=TPAD_Get_Val();
delay_ms(10);
}
for(i=0;i<9;i++)//排序
{
for(j=i+1;j<10;j++)
{
if(buf[i]>buf[j])//升序排列
{
temp=buf[i];
buf[i]=buf[j];
buf[j]=temp;
}
}
}
temp=0;
for(i=2;i<8;i++)temp+=buf[i];//取中間的 8 個數據進行平均
tpad_default_val=temp/6;
printf("tpad_default_val:%d\r\n",tpad_default_val);
if(tpad_default_val>(vu16)TPAD_ARR_MAX_VAL/2)return 1;
//初始化遇到超過 TPAD_ARR_MAX_VAL/2 的數值,不正常!
return 0;
}
//復位一次
//釋放電容電量,並清除定時器的計數值
void TPAD_Reset(void)
{
GPIO_InitTypeDef GPIO_Initure;
GPIO_Initure.Pin=GPIO_PIN_1; //PA1
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推輓輸出
GPIO_Initure.Pull=GPIO_PULLDOWN; //下拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_RESET); //PA1 輸出 0,放電
delay_ms(5);
__HAL_TIM_CLEAR_FLAG(&TIM5_Handler,TIM_FLAG_CC2|TIM_FLAG_UPDATE);
//清除標誌位
__HAL_TIM_SET_COUNTER(&TIM5_Handler,0); //計數器值歸 0
GPIO_Initure.Mode=GPIO_MODE_AF_INPUT; //推挽復用輸入
GPIO_Initure.Pull=GPIO_NOPULL; //不帶上下拉
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
}
//得到定時器捕獲值
//如果超時,則直接返回定時器的計數值.
//返回值:捕獲值/計數值(超時的情況下返回)
u16 TPAD_Get_Val(void)
{
TPAD_Reset();
while(__HAL_TIM_GET_FLAG(&TIM5_Handler,TIM_FLAG_CC2)==RESET)
//等待捕獲上升沿
{
if(__HAL_TIM_GET_COUNTER(&TIM5_Handler)>TPAD_ARR_MAX_VAL-500)
return __HAL_TIM_GET_COUNTER(&TIM5_Handler);//超時了,直接返回 CNT 的值
};
return HAL_TIM_ReadCapturedValue(&TIM5_Handler,TIM_CHANNEL_2);
}
//讀取 n 次,取最大值
//n:連續獲取的次數
//返回值:n 次讀數裡面讀到的最大讀數值
u16 TPAD_Get_MaxVal(u8 n)
{
u16 temp=0;
u16 res=0;
u8 lcntnum=n*2/3;//至少 2/3*n 的有效個觸摸,才算有效
u8 okcnt=0;
while(n--)
{
temp=TPAD_Get_Val();//得到一次值
if(temp>(tpad_default_val*5/4))okcnt++;//至少大於默認值的 5/4 才算有效
if(temp>res)res=temp;
}
if(okcnt>=lcntnum)return res;//至少 2/3 的概率,要大於默認值的 5/4 才算有效
else return 0;
}
//掃描觸摸按鍵
//mode:0,不支持連續觸發(按下一次必須鬆開才能按下一次);1,支持連續觸發(可以一直按
下)
//返回值:0,沒有按下;1,有按下;
u8 TPAD_Scan(u8 mode)
{
static u8 keyen=0; //0,可以開始檢測;>0,還不能開始檢測
u8 res=0;
u8 sample=3; //默認採樣次數為 3 次
u16 rval;
if(mode)
{
sample=6; //支持連按的時候,設置採樣次數為 6 次
keyen=0; //支持連按
}
rval=TPAD_Get_MaxVal(sample);
if(rval>(tpad_default_val*4/3)&&rval<(10*tpad_default_val))
//大於 tpad_default_val+(1/3)*tpad_default_val,且小於 10 倍 tpad_default_val,則有效
{
if(keyen==0)res=1; //keyen==0,有效
//printf("r:%d\r\n",rval);
keyen=3; //至少要再過 3 次之後才能按鍵有效
}
if(keyen)keyen--;
return res;
}
//定時器 5 通道 2 輸入捕獲配置
//arr:自動重裝值(TIM2 是 16 位的!!)
//psc:時鐘預分頻數
void TIM5_CH2_Cap_Init(u32 arr,u16 psc)
{
TIM_IC_InitTypeDef TIM5_CH2Config;
TIM5_Handler.Instance=TIM5; //通用定時器 5
TIM5_Handler.Init.Prescaler=psc; //分頻
TIM5_Handler.Init.CounterMode=TIM_COUNTERMODE_UP; //向上計數器
TIM5_Handler.Init.Period=arr; //自動裝載值
TIM5_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
HAL_TIM_IC_Init(&TIM5_Handler);
TIM5_CH2Config.ICPolarity=TIM_ICPOLARITY_RISING; //上升沿捕獲
TIM5_CH2Config.ICSelection=TIM_ICSELECTION_DIRECTTI;//映射到 TI1 上
TIM5_CH2Config.ICPrescaler=TIM_ICPSC_DIV1; //配置輸入分頻,不分頻
TIM5_CH2Config.ICFilter=0; //配置輸入濾波器,不濾波
HAL_TIM_IC_ConfigChannel(&TIM5_Handler,&TIM5_CH2Config,TIM_CHANNEL_2);
//配置 TIM5 通道 2
HAL_TIM_IC_Start(&TIM5_Handler,TIM_CHANNEL_2); //開始捕獲 TIM5 的通道 2
}
//定時器 5 底層驅動,時鐘使能,引腳配置
//此函數會被 HAL_TIM_IC_Init()調用
//htim:定時器 5 句柄
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_TIM5_CLK_ENABLE(); //使能 TIM5 時鐘
__HAL_RCC_GPIOA_CLK_ENABLE(); //開啟 GPIOA 時鐘
GPIO_Initure.Pin=GPIO_PIN_1; //PA1
GPIO_Initure.Mode=GPIO_MODE_AF_INPUT; //推挽復用輸入
GPIO_Initure.Pull=GPIO_NOPULL; //不帶上下拉
GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH; //高速
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
}
函數TIM5_CH2_Cap_Init和上一章輸入捕獲實驗中函數TIM5_CH1_Cap_Init的配置過程幾
乎是一模一樣的,不同的是上一章實驗開 TIM5_CH1_Cap_Init 函數最後調用的是函數
HAL_TIM_IC_Start_IT,使能輸入捕獲通道的同事開啟了輸入捕獲中斷,而該函數最後調用的
函數是 HAL_TIM_IC_Start,只是開啟了輸入捕獲通道,並沒有開啟輸入捕獲中斷。
函數 HAL_TIM_IC_MspInit 是輸入捕獲通用 MSP 回調函數,該函數的作用是使能定時器
和 GPIO 時鐘,配置 GPIO 復用映射關係。該函數功能和輸入捕獲實驗中該函數作用基本類似。
函數 TPAD_Get_Val 用於得到定時器的一次捕獲值。該函數先調用 TPAD_Reset,將電容放
電,同時設置通過程序__HAL_TIM_SET_COUNTER(&TIM5_Handler,0)將計數值 TIM5_CNT 設
置為 0,然後死循環等待發生上升沿捕獲(或計數溢出),將捕獲到的值(或溢出值)作為返回
值返回。
函數 TPAD_Init 用於初始化輸入捕獲,並獲取默認的 TPAD 值。該函數有一個參數,用來
傳遞分頻係數,其實是為了配置 TIM5_CH2_Cap_Init 的計數周期。在該函數中連續 10 次讀取
TPAD 值,將這些值升序排列後取中間 6 個值再做平均(這樣做的目的是儘量減少誤差),並賦
值給 tpad_default_val,用於後續觸摸判斷的標準。
函數 TPAD_Scan 用於掃描 TPAD 是否有觸摸,該函數的參數 mode,用於設置是否支持連
續觸發。返回值如果是 0,說明沒有觸摸,如果是 1,則說明有觸摸。該函數同樣包含了一個靜
態變量,用於檢測控制,類似第七章的 KEY_Scan 函數。所以該函數同樣是不可重入的。在函
數中,我們通過連續讀取 3 次(不支持連續按的時候)TPAD 的值,取最大值和 tpad_default_val*4/3
比較,如果大於則說明有觸摸,如果小於,則說明無觸摸。其中 tpad_default_val 是我們在調用
TPAD_Init 函數的時候得到的值,然後取其 4/3 為門限值。該函數,我們還做了一些其他的條件
限制,讓觸摸按鍵有更好的效果,這個就請大家看代碼自行參悟了。
函數 TPAD_Reset 顧名思義,是進行一次復位操作。先設置 PA1 輸出低電平,電容放電,
同時清除中斷標誌位並且計數器值清零,然後配置 PA1 為復用功能浮空輸入,利用外部上拉電
阻給電容 Cs(Cs+Cx)充電,同時開啟 TIM5_CH2 的輸入捕獲。
函數 TPAD_Get_MaxVal 就非常簡單了,它通過 n 次調用函數 TPAD_Get_Val 採集捕獲值,
然後進行比較後獲取 n 次採集值中的最大值。
tpad.h 頭文件部分代碼比較簡單,這裡不做介紹。
接下來我們看看主函數代碼如下:
int main(void)
{
u8 t=0;
HAL_Init(); //初始化 HAL 庫
Stm32_Clock_Init(RCC_PLL_MUL9); //設置時鐘,72M
delay_init(72); //初始化延時函數
uart_init(115200); //初始化串口
LED_Init(); //初始化 LED
TPAD_Init(6); //初始化觸摸按鍵
while(1)
{
if(TPAD_Scan(0)) //成功捕獲到了一次上升沿(此函數執行時間至少 15ms)
{
LED1=!LED1; //LED1 取反
}
t++;
if(t==15)
{
t=0;
LED0=!LED0; //LED0 取反,提示程序正在運行
}
delay_ms(10);
}
}
該 main 函數比較簡單,TPAD_Init(6)函數執行之後,就開始觸摸按鍵的掃描,當有觸摸的
時候,對 DS1 取反,而 DS0 則有規律的間隔取反,提示程序正在運行。注意在修改 main 函數
之後,還需要在 main.c 裡面添加 tpad.h 頭文件,否則會報錯哦。
這裡還要提醒一下大家,不要把 uart_init(115200);去掉,因為在 TPAD_Init 函數裡面,我們
有用到 printf,如果你去掉了 uart_init,就會導致 printf 無法執行,從而死機。
至此,我們的軟體設計就完成了。
16.4 下載驗證
在完成軟體設計之後,將我們將編譯好的文件下載到戰艦 STM32 V3 上,可以看到 DS0 慢
速閃爍,此時,我們用手指觸摸 ALIENTEK 戰艦 STM32F103 開發板上的 TPAD(右下角的白
色頭像),就可以控制 DS1 的亮滅了。不過你要確保 TPAD 和 ADC 的跳線帽連接上了哦!如圖
16.4.1 所示:
同時大家可以打開串口調試助手,每次復位的時候,會收到 tpad_default_val 的值,一般為195 左右。