用 CH549 的电容触摸按键检测功能测量水位
修改声明
由于 P1.4 连接着 USB type-C 接口的 CC,容易受其影响导致无法测量。gitee 的工程中已将引脚改为 P1.0,ADC 通道 改为 0。由于作者的懒惰,本文并没有做对应的修改,请读者注意 🙂
目录
 
赶时间的同学可以只看搭建和总结
搭建
必选部分

- 一个不导电的容器,最好是柱状的。我用的是量筒。如上图
 
- 往容器上贴铜箔胶带,铝箔胶带也行。都没有?拿点金属片锡纸牛奶盒,剪剪贴上去也行。注意两片金属要大致平行,不能接触。
 
- 从金属片上引线出来,我直接用胶带把杜邦线压在了铜箔上。如图
 

- 一根引线接 GND,另一根接 P1.4 (建议用 P1.0,见修改声明)。如图
   

  
可选部分(显示)

  
原理
从电容到水位
  
我们在容器上贴的两个金属片和中间的容器形成了一个电容器(以下称为 Cx)。  
水的电容率(介电常数)比空气大得多;所以容器里有水时的 Cx 会比空容器的 Cx 大。而且水越多 Cx 越大。  
Cx 反映了水位的情况。所以只要测出 Cx,我们就能从中得到水位的情况。
  
如何测电容
  
 CH549电容按键检测模块框图,截取自沁恒文档-CH549DS1.PDF↑
  
CH549 支持电容式触摸按键检测,这个功能也可以用来大致测量电容值的大小。  
其原理是:芯片先把待测引脚接地给电容放电,放干净电之后,再通过一个电阻给引脚充电一段时间,接着用ADC采集引脚上的电压。采集到的电压越高,引脚上的电容值就越小。
这个方法不容易测出精确的电容值,但用于一般水位检测还是可以的。
程序
知道了如何测电容,从电容可以得到水位,我们就可以去构建我们的程序啦。  
下面是程序的框图

初始化
首先要初始化,把检测引脚配置成高阻输入,ADC 初始化就可以了。
我写了一个函数,可以根据输入的 ADC 通道号去配置引脚:
void WLM_init( UINT8 ch )
{
    //Touch采样通道设置为高阻输入
    if(ch<=7)                                  //P10~P17引脚配置
    {
        P1_MOD_OC &= ~(1<<ch);                 //高阻输入
        P1_DIR_PU &= ~(1<<ch);
    }
    if(ch>7 && ch<=0x0f)                       //P00~P07引脚配置
    {
        P0_MOD_OC &= ~(1<<(ch-8));             //高阻输入
        P0_DIR_PU &= ~(1<<(ch-8));
    }
    ADC_CFG |= (bADC_EN|bADC_AIN_EN);                //开启ADC模块电源,选择外部通道
    ADC_CFG = ADC_CFG & ~(bADC_CLK0 | bADC_CLK1);    //选择ADC参考时钟
    ADC_CHAN = (3<<4);                               //默认选择外部通道0
    ADC_CTRL = bADC_IF;                              //清除ADC转换完成标志,写1清零
}
测量
一个电容按键检测的函数,返回检测到的电压值。  
通过写 TKEY_CTRL 寄存器可以配置给引脚充电的时间,这个时间要根据你的实际情况选择,以空容器情况下测得 3500 左右的电压值为宜
int touchKey( UINT8 ch,UINT8 chargePulseWidth )
{
    ADC_CHAN = ADC_CHAN & (~MASK_ADC_CHAN) | ch; //外部通道选择
    TKEY_CTRL = chargePulseWidth;                //充电脉冲宽度配置,仅低7位有效(写入的同时会自动启动一次电容按键检测)
    while(ADC_CTRL&bTKEY_ACT);                   //等待检测完成
    int ret = (ADC_DAT&0xFFF);                   //取低12位,只有低12位是有效数据
    return ret;
}
有了检测代码之后,只要检测引脚,用返回的电压值就可以反映水位的情况了:数值大,水位低;数值小,水位高。  
多采样几次,取平均会让数据更加稳定:
long val = 0;						//存储大数 int 会溢出,所以用 long
for(int j = 0;j<20000;j++){			//采样 20000 次
    val += touchKey(WL_CH,WL_CPW);
}
val /= 20000;						//取平均
printf_fast_f("Value : %ld.",val);  //printf_fast_f 可以输出浮点数,而且运行更快
数据转换
电压数据虽然能反映水位高低,但一点都不直观,有没有办法让它直观一点呢?  
我们可以通过校准,把电压数据转换为水量。  
测出空容器时的电压读数 V0 
容器装满水(或在某个水位)时的电压读数 V1  
电压跟水量的近似关系就出来了  
  
这样测到电压是 V 时,可以通过下面的公式将 V 转为水量:
水量 = \frac{V-V_0}{V_1 - V_0} * 最大水量 
写成代码就是:
// 把电压采样值 转为 水位
float WLM_val2level(int val){
	float waterLevel;
	int i;
	if(val > V0) return 0;	// 电容值比空容器还小,视为无水
	if(val > V1){
		float waterLevel = WL_MAX  * (val - V0)/(V1 - V0);
		return waterLevel;
	}
	return WL_MAX;			//电容值比满水还大,视为满水
}
总结
  
装置合照↑
温度不变时,5mL 电子量筒校准后的误差在 ± 0.2mL 以内,但芯片温度变化对测量值的影响很大。  
原因可能是芯片内的充电电阻 (电容按键检测模块框图中的 Itkey )由半导体制成,其阻值随温度变化很大。  
如需排除环境温度的影响,还需做温度补偿,这是程序待完善的地方。
本文中的项目工程放在 gitee 上:  
由于 P1.4 连接着 USB type-C 接口的 CC,容易受其影响导致无法测量。gitee 的工程中已将引脚改为 P1.0,ADC 通道 改为 0。由于作者的懒惰,本文并没有做对应的修改,请读者注意 🙂
使用方法:
- 调节 WL_CPW 使最低水位的电压采样值在 3500 左右
 
- 容器装满水,记下电压采样值
 
- 修改WL_MIN_VAL,WL_MAX_VAL,WL_MAX,WL_MIN,WL_UNIT
 
如果你自己建工程,OLED 显示程序的引脚需要更改为:
//CH549_sdcc.h
//SSD1306的SPI接口定义
#define OLED_CS     P3_0 //片选
#define OLED_RST    P3_1 //复位
#define OLED_DC     P3_3 //数据/命令控制
#define OLED_SCL    P1_7 //D0(SCLK) 时钟 
#define OLED_SDIN   P1_5 //D1(MOSI) 数据
下面是加入显示代码后的程序,这个程序里最低水位可以不为0
/***************************Encoding = UTF-8***********************************
 * WaterLevelMeasuring
 * main.c
 * 
 * 水位测量程序
 * 在OLED屏上输出电压采样值和水位
 * 使用 SSD1306 OLED 屏,接线如下
 * 
 * 水位测量引脚 - P1.4
 * 屏幕的接法见 CH549_OLED.h - //SSD1306的SPI接口定义
 * 
 * 使用方法:
 * 1.调节 WL_CPW 使最低水位的电压采样值在 3500 左右
 * 2.容器装满水,记下电压采样值
 * 3.修改WL_MIN_VAL,WL_MAX_VAL,WL_MAX,WL_MIN,WL_UNIT
 * 
 * 适用于 VeriMake CH549DB 1.x.x
 * -- by Benue@Verimake
*******************************************************************************/
#include <CH549_OLED.h>    
#include <CH549_SPI.h> 
#include <CH549_ADC.h>
#include <CH549_DEBUG.h>
#include <CH549_sdcc.h>
#define WL_MAX 5.0		//Maximun water level,最高水位
#define WL_MAX_VAL 3025	//最高水位对应的电压采样值
#define WL_MIN 0.0		//Minimun water level,最低水位
#define WL_MIN_VAL 3480	//最低水位对应的电压采样值
#define WL_UNIT ("mL")	//Unit of water level,水位的单位
#define WL_CH 4			//Measuring channel, 测量用的 ADC 通道
#define WL_PIN ( P1_4 )	//Measuring Pin,测量用的引脚,应与 ADC 通道相匹配
#define WL_CPW 40		//Pulse width for charging the touch key pin,in 2/Fsys,充电时间,单位为2个系统时钟周期
int oled_colum;
int oled_row;
void setCursor(int column,int row);
//chargePulseWidth : Pulse width for charging the touch key pin from low to high,in 2/Fsys
int touchKey( UINT8 ch,UINT8 chargePulseWidth )
{
    ADC_CHAN = ADC_CHAN & (~MASK_ADC_CHAN) | ch;     //外部通道选择
    //电容较大时可以先设置IO低,然后恢复浮空输入实现手工放电,≤0.2us
    TKEY_CTRL = chargePulseWidth;                         //充电脉冲宽度配置,仅低7位有效(同时清除bADC_IF,启动一次TouchKey)
    while(ADC_CTRL&bTKEY_ACT);
	int ret = (ADC_DAT&0xFFF);
    return ret;
}
void WLM_init( UINT8 ch )
{
	//Touch采样通道设置为高阻输入
    if(ch<=7)                                  //P10~P17引脚配置
    {
        P1_MOD_OC &= ~(1<<ch);                 //高阻输入
        P1_DIR_PU &= ~(1<<ch);
    }
    if(ch>7 && ch<=0x0f)                       //P00~P07引脚配置
    {
        P0_MOD_OC &= ~(1<<(ch-8));             //高阻输入
        P0_DIR_PU &= ~(1<<(ch-8));
    }
    ADC_CFG |= (bADC_EN|bADC_AIN_EN);                //开启ADC模块电源,选择外部通道
    ADC_CFG = ADC_CFG & ~(bADC_CLK0 | bADC_CLK1);    //选择ADC参考时钟
    ADC_CHAN = (3<<4);                               //默认选择外部通道0
    ADC_CTRL = bADC_IF;                              //清除ADC转换完成标志,写1清零
}
// 把电压采样值 转为 水位
float WLM_val2level(int val){
	float waterLevel;
	int i;
	if(val > WL_MIN_VAL) return 0;
	if(val > WL_MAX_VAL){ 
		float estimate = (float)(WL_MAX - WL_MIN) * (val - WL_MIN_VAL)/(WL_MAX_VAL - WL_MIN_VAL);
		waterLevel = WL_MIN + estimate;
		return waterLevel;
	}
	return WL_MAX;
}
int samples[100];
void main()
{
	int i;
    CfgFsys( );           //CH549时钟选择配置
    mDelaymS(20);
	CH549UART1Init();
	
	SPIMasterModeSet(3);  //SPI主机模式设置,模式3
    SPI_CK_SET(12);       //设置SPI sclk 时钟信号为12分频
	OLED_Init();		  //初始化OLED  
	OLED_Clear();         //将OLED屏幕上内容清除
	setFontSize(16);      //设置文字大小
    WLM_init(4);
	OLED_Clear();
	while(1){
		long val;
		setCursor(0,0);
		printf_fast_f("Water Level");
		for(int j = 0;j<20000;j++){
			val += touchKey(WL_CH,WL_CPW);
		}
		val /= 20000;
		setCursor(0,2);
		printf_fast_f("Level: %.1f%s",WLM_val2level(val),WL_UNIT);
		setCursor(0,4);
		printf_fast_f("Value: %ld",val);
		mDelaymS(200);
	}
}
/********************************************************************
* 函 数 名       : putchar
* 函数功能       : 将printf映射到OLED屏幕输出上
* 输    入      : 字符串
* 输    出    	: 字符串
********************************************************************/
int putchar( int a)
{      
    //在光标处显示文字 a
    OLED_ShowChar(oled_colum,oled_row,a);
    //将光标右移一个字的宽度(8列),以显示下一个字
    oled_colum+=8;
    
    /*当此行不足以再显示一个字时,换行.
    即行坐标+2(下移1个字的高度,2行=16像素).
    同时光标回到最边(列坐标=0).
    */
    if (oled_colum>120){oled_colum=0;oled_row+=2;}
    return(a);
}
/********************************************************************
* 函 数 名       : setCursor
* 函数功能		   : 设置光标(printf到屏幕上的字符串起始位置)
* 输    入       : 行坐标 列坐标(此处一行为8个像素,一列为1个像素,所以屏幕上共有8行128列)
* 输    出    	 : 无
********************************************************************/
void setCursor(int column,int row)
{
    oled_colum = column;
    oled_row = row;
}