用 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;
}