自制导纳信号发生器 [原创cnblogs.com/helesheng]
最近正在研制一种通过测量人体导纳,估算体内血液变化率,进而评估心血管系统泵血功能的医疗仪器。为测量人体导纳,我们设计了一套巧妙的激励信号幅度反馈电路,该电路由于涉及商业机密就不在这里讨论了。这里主要分享一下我自己设计的,用于对导纳测量电路进行调试和幅度定标的重要工具——导纳信号发生器的设计。
以下原创内容欢迎网友转载,但请注明出处:http://blog.163.com/helesheng
一、自制导纳信号发生器的原因
在研制人体导纳测试仪器的过程中,我发现很难对仪器进行调试和定标:由于无法买到商品化的导纳信号发生器,只能直接进行人体试验。一方面,每次调试电路都很麻烦;另一方面,无法对电路增益进行幅度和频率定标。因此自制一种阻值接近人体,变化频率可调的导纳信号发生器就势在必行了。
导纳(admittance)的定义是阻抗的倒数,标准量纲为西门子S,1S即1Ω的阻抗对应的导纳.
“导纳信号发生器”从本质上讲也是阻抗信号发生器,但“导纳信号发生器”的静态部分应该可以直接设置导纳值(而非阻值),而动态部分则应该是导纳随时间成正弦变化的。
先来看看人体导纳的基本情况:在50KHz交流信号激励下,人体胸腔静态阻抗约30Ω(33mS,33毫西门子),由于血流变化引起的阻抗约为5KΩ(0.2mS)。两者相差较大,为方便调试,我用固定电阻实现导纳信号发生器的静态部分,血流变化引起的动态导纳变化则用数字电位器(Digital Potentiometer)来模拟。“动态”和“静态”两部分电路则采用并联形式。
二、静态部分的电路
我设计的导纳信号发生器的“静态部分”,是指导纳值可以手动调节,但不会自动变化的部分。其电路如下。
图1 导纳信号发生器的静态部分电路
其中R1~R10都是阻值为100Ω,精度为1%的电阻,OPT1-OPT10则是导通电阻仅为1Ω的固态继电器KAQY212。导纳信号发生器可以在使用者的控制下,使OPT1~OPT10导通和关闭,每多打开一个固态继电器则测试端Ts1和Ts2之间的导纳就增加10mS(100Ω的倒数)。设上述电路在Ts1和Ts2之间产生的静态导纳为
其中K为打开的固态继电器数量, 是100Ω电阻对应的导纳数值。例如,当打开三个固态继电器后,
为30mS —— 与人体静态导纳相当。
三、动态部分的电路
我设计的导纳信号发生器的“动态部分”,是导纳值自动呈正弦性周期变化的部分,它与静态部分并联,以模拟人体的动态导纳变化。我用最大阻值为10KΩ的数字电位器MCP41010来实现动态部分。
图2 与静态部分并联的动态部分电路
MCP41010是SPI接口的器件,使用者设置完变化频率后,导纳信号发生器中的MCU通过SPI口定制改变MCP41010的阻值,以使导纳值根据设定的频率成正弦变化。其中,MCP41010的阻值分辨率为最大阻值(10KΩ)的256分之一。
设MCP41010在Ts1和Ts2之间产生的动态导纳为 ,而从外部观测整个导纳信号发生器,其总体导纳可以表示为下式。
四、控制和人机交互电路
我的导纳信号发生器能够设置静态导纳值和动态导纳变化的频率,因此还必须有显示和按键等人机交互设备。为了省事,我使用了一个具有Arduino接口的STM32F103开发板作为我的主控板。扩展Arduino盾板除了有上述的静态导纳和动态导纳电路之外,还有一只四位数码管和四只按键。四位数码管中两位用于显示静态导纳,两位用于显示动态导纳变化的频率;四只按键两只用于调整静态导纳,两只用于调整动态导纳变化的频率。
标准Arduino接口的I/O数量不多,不足以控制这么多外设,因此使用了三只74HC595来扩展I/O口。具体电路如下图所示。
图3 控制和人机交互电路
上面电路右上角为标准Arduino扩展接口,左边顺序串接的三只74HC595扩展产生:数码管的段码驱动D0-D7、数码管的位选通驱动DIG0-DIG3以及静态导纳电路中固态继电器的开关信号SWs1- SWs10。
动态导纳产生电路中的数字电位器MCP41010则由Arduino接口中的SPI接口控制。
由上述电路配置可知,Arduino控制板中的MCU除了要定时显示数码管的各个位之外,还要定时刷新MCP41010的阻值,以及扫描按键。我在实时操作系统uC/OS-II下来开发导纳信号发生器的软件。
五、动态正弦导纳信号的产生算法
以下是本文的核心内容。
1)正弦导纳表格的产生
既然是“导纳信号发生器”,就应该使测试端子Ts+和Ts-之间的动态导纳成正弦变化。但作为一款“数字电位器”,MCP41010是将自己的总阻值(10KΩ±2KΩ)均分为256份。MCU通过指令给MCP41010的数值每增加“1”,其抽头和某一端的阻值就增加约40Ω,即每个LSB对应的阻值相等,而非导纳相等。
显然当阻值较小时,数字电位器的每个LSB变化所引起的导纳变化较大。因此在数字电位阻值处,导纳分辨率也较低。通过一段Matlab代码来计算产生正弦导纳所需的数字电位数值。
% 本脚本用于产生导纳模拟器数字电位器所需的数值
%数字电位MCP41010的阻值为1-10k欧姆,取中间阻值作为导纳正弦变化的0值
MAX_R = 10*10^3;
MID_R = MAX_R*0.5;%MID_R决定了计算产生的导纳以哪个值为中心
MID_ADMIT = 1/MID_R;%导纳的平均值(中间值)
MIN_ADMIT = 1/MAX_R;%导纳的最小值等于最大的阻值的倒数
%AMP_ADMIT = (MID_ADMIT - MIN_ADMIT)*0.98;%导纳的变化幅度为最大幅度的98%
AMP_ADMIT = (MID_ADMIT - MIN_ADMIT)*0.8;%只使用了导纳的变化幅度的80%
N = 64;%表格中的数据点数
i = 1 : N;
sig_admit = AMP_ADMIT*sin(2*pi*i/N) + MID_ADMIT;
R_data8_admin = round(sig_admit.^(-1));%计算产生正弦变化的导纳所需的阻值
plot(R_data8_admin,'*-r');grid on;
title('导纳正弦变化所需的阻值(单位欧姆)');
%将上述阻值折算为MCP41010所需的0~255的设定值
R_CODE_MCP4XXXX = round((R_data8_admin/MAX_R)*256);
%反过来计算这些数值所对应的导纳值
admit_true = (R_CODE_MCP4XXXX/256 * MAX_R).^(-1);
figure();
plot(admit_true,'*-b');grid on;
title('MCP41010变化引起的实际导纳变化(单位西门子)');
上面的代码取MCP41010阻值范围的一个值MID_R对应的导纳MID_ADMIT作为要产生的正弦导纳值的平均值。由于正弦导纳信号中高于平均值的部分的导纳值和低于平均值的部分的导纳值是对应相等的,但数字电位器阻值较小的部分所对应的导纳范围显然较宽,因此用正半周内最大导纳值MAX_ADMIT减去平均值MID_ADMIT得到的导纳变化幅度的一部分作为正弦导纳信号的幅度AMP_ADMIT。
AMP_ADMIT = (MID_ADMIT - MIN_ADMIT)*0.8;%使用了导纳的变化幅度的80%
取MID_ADMIT为MCP41010的中间阻值所对应的导纳,而AMP_ADMIT为可能的最大振幅的80%。
上面代码产生的“导纳正弦变化所需的阻值(单位欧姆)”如下图。
图4 导纳正弦变化所需的阻值
从图中可知,在阻值的较小的一半(上图下半截),较小的阻值变化就能引起和阻值较大的一半(上图上半截)相同的导纳变化。因此为产生上下对称的导纳值,上图的下半截被压缩得“较窄”。如前所述,这一现象将导致在阻值较小的下半段,数字电位的分辨率不足。上述Matlab代码对这一现象进行了仿真,得到了下图所示的“MCP41010变化引起的实际导纳变化(单位西门子)”。从图中可知导纳发生器的产生的正弦变化幅度为(2.0±0.5)×10-4S,简单表示为:
图5 MCP41010变化引起的实际导纳变化
可以看到在阻值较小的上半部分,导纳变化的正弦曲线存在明显失真。若感觉这种程度的失真无法达到设计要求,则只能更换分辨率更高的数字电位器了。
2)程控导纳变化频率的实现
人体导纳变化主要由心脏搏动引起,因此变化频率在10Hz以内。为了对不同频率的导纳变化进行频率定标,需要导纳信号发生器能够产生频率稳定且可调的导纳变化信号。提到频率可调,自然想到DDS算法。DDS算法可以描述为下列公式。
其中,fout是算法输出的信号频率,fclk是算法刷新的速度,2N是DDS算法查找表(LUT)的长度,而delta则是算法在查找表中每次跳过的点数。程序只需要修改M就可以产生和delta成正比的输出频率fout。每次在为方便uC/OS-II下的程序设计,我将uC/OS-II的系统时钟设为1KHz,并把刷新MCP41010的阻值的操作放在钩子函数OSTimeTickHook();中,DDS的时钟fclk也就是1KHz。取N为16,即查找表的长度为2N =65536。根据公式(5),fout的分辨率约为0.015Hz——远高于频率定标所需的频率精度。钩子函数中DDS算法的实现代码如下。
void OSTimeTickHook (void)
{
unsigned char MCP41XXX_1st_byte,MCP41XXX_2nd_byte;//需要通过SPI口发送给MCP41XXXX的两个字节
MCP41XXX_1st_byte = 0X11;//对电位器1执行写数据操作
MCP41XXX_2nd_byte = 0x00;//第二个字节是需要写入的电阻数值
/////以下DDS算法产生频率可调节的正弦导纳值////////////
unsigned short delta;//DDS算法的地址表增量
delta = admit_frq*/;//根据DDS算法,地址表增量等于目标频率admit_frq乘以表格长度L,再除以采样率fs
dds_adder = dds_adder + delta;//累加器增加并自然溢出
MCP41XXX_2nd_byte = - sin_admit_tbl[dds_adder>>];//数值表格只有64个数,也就是6位地址可以覆盖
//最后用256减去查表的值是因为电路图画的有问题:输入MCP41XXX的数值是PW和PB之间的值,
//但电路图将MCP41XXX连接成变阻器的方法是将PW和PB短路,从而外部得到的是PW和PA的值,所以将数值反转后才能得到正确的值
CS_RP = ;
SPIx_ReadWriteByte(MCP41XXX_1st_byte);
SPIx_ReadWriteByte(MCP41XXX_2nd_byte);
CS_RP = ;
}
钩子函数
sin_admit_tbl[]是上面的Matlab代码产生的长度为64个点的MCP41010数值表,它们对应的导纳值是一个正弦变化的周期。其索引dds_adder长度是16位的,但sin_admit_tbl[]的地址只有6位(64个点),因此将dds_adder右移10位作为sin_admit_tbl[]的地址。admit_frq是DDS算法希望产生的信号频率,相当于(5)式中的fout。admit_frq作为全局变量,其值是在键盘任务中修改的。
6、uC/OS-II下代码的实现
由于刷新数字电位的任务在钩子函数中完成,uC/OS-II中只需要两个任务:1)“键盘任务”TaskKEY();负责扫描四个按键,并根据输入刷新静态导纳r_sw_num和动态导纳频率admit_frq这两个全局变量。2)“刷新显示和输出任务”TaskFLASH_DIS();负责定时地、逐位地刷新数码管上显示的内容,以及静态导纳电路中需要打开的固态继电器。
键盘任务代码如下所示。
void TaskKEY(void *pdata)
{
while()
{
OSTimeDly();
if(KEY1 == )
{
OSTimeDly();//消除按键抖动
if(KEY1 == )
{
if(admit_frq < MAX_FRQ)
admit_frq++;
while(KEY1 == )OSTimeDly();//一直等待到按键被释放,最后的延时还能消抖动
}
}
if(KEY2 == )
{
OSTimeDly();//消除按键抖动
if(KEY2 == )
{
if(admit_frq > MIN_FRQ)
admit_frq--;
while(KEY2 == )OSTimeDly();//一直等待到按键被释放,最后的延时还能消抖动
}
}
if(KEY3 == )
{
OSTimeDly();//消除按键抖动
if(KEY3 == )
{
if(r_sw_num < MAX_SW)
r_sw_num++;
while(KEY3 == )OSTimeDly();//一直等待到按键被释放,最后的延时还能消抖动
}
}
if(KEY4 == )
{
OSTimeDly();//消除按键抖动
if(KEY4 == )
{
if(r_sw_num > )
r_sw_num--;
while(KEY4 == )OSTimeDly();//一直等待到按键被释放,最后的延时还能消抖动
}
}
}
}
键盘任务
刷新显示和输出任务代码如下所示。其中显示缓存dis_buff[]中对应的是需要显示的每个数码管位的内容。dis_buff[]的内容需要不断计算、刷新,以防键盘任务在用户操作时修改需要显示的值。变量first_byte,second_byte,third_byte中的值则是需要通过串行口下载到三只74HC595中的。其中既包括当前需要显示的数码管位的字形码D0-D7,也包括显示的位置选通信号DIG0-DIG3和固态继电器的开关信号SWs1- SWs10,其对应关系请参见图3中电路网络标号。
void TaskFLASH_DIS(void *pdata)
{
unsigned char dis_buff[],i;
unsigned short sw_ctl_bits=;//低10位对应100欧姆导纳电阻的开关状态
unsigned char dis_index = ;//刷新数码管到第几位的计数器
unsigned char bit_sel = ;//决定哪位被选中显示,必须有一个位为1
unsigned char first_byte,second_byte,third_byte;
while()
{
//////////先刷新需要595输出的所有东西的数值///////////
dis_buff[] = r_sw_num/;//数码管的右半边两个显示打开的100欧姆电阻数目
dis_buff[] = r_sw_num%;
if(dis_buff[] == )//处理十位的消隐问题
dis_buff[] = LED_PATT[];//消隐显示
else
dis_buff[] = LED_PATT[dis_buff[]];//查字型表
dis_buff[] = LED_PATT[dis_buff[]];//查字型表
dis_buff[] = admit_frq/;//数码管的左半边两个显示导纳变化的频率
dis_buff[] = admit_frq%;
if(dis_buff[] == )//处理十位的消隐问题
dis_buff[] = LED_PATT[];//消隐显示
else
dis_buff[] = LED_PATT[dis_buff[]];//查字型表
dis_buff[] = LED_PATT[dis_buff[]];//查字型表
//由打开的100欧姆电阻数r_sw_num,决定打开的位
sw_ctl_bits = ;
if(r_sw_num != )
{
for(i = ;i < r_sw_num;i++)
{
sw_ctl_bits = sw_ctl_bits<<;
sw_ctl_bits++;//将最低位置一,并左移一位
}
}
bit_sel = 0x01 << dis_index;//刷新到哪一位,就让对应的位选信号为1
//////////刷新595输出,每次让一个数码管亮///////////
third_byte = dis_buff[dis_index];//第三个送出的字节是字形码,但具体是那个位的,要由当前扫描到的位置决定
///////第二个字节的最高两个位控制100欧姆导纳电阻的最低两个开关,第四位是数码管的位选通信号
second_byte = (sw_ctl_bits & 0x0003);//第二个字节中只去最低两个位
second_byte = second_byte << ;//将最低两个位放到最高两个位置
second_byte = second_byte + bit_sel;//第四位是位选通信号
first_byte = (sw_ctl_bits >> ) & 0x00ff;//取开关控制位的3-10位
send_to_tri_74595(first_byte,second_byte,third_byte);//从模拟SPI口送出三个准备好的字节
if(dis_index < )//更新下一个需要刷新的位置
dis_index++;
else
dis_index = ;
OSTimeDly();//刷新速度是5个时钟周期也就是200Hz
}
}
刷新显示和输出任务
7、验证电路
常见的万用表、示波器等仪表通常可以直接测量阻值和电压值,不能直接测量导纳和电流。因此需要一个电路来验证导纳信号发生器产生的信号是否符合设计要求。采用下列由运算放大器为主构成的电路来将导纳值变换成电压值,来验证上述设计的正确性。
图6 将导纳转换为方便测量和观察的电压信号的电路
其中,RL是导纳信号发生器。VREF_-0.1V是由电压基准芯片分压后,再由跟随器产生的标准-0.1V电压。使用时尤其要注意,导纳信号发生器的GND和这里的GND不是同一GND,导纳信号发生器和上图一定要分开供电(比如,上图电路采用电池供电)否则一定会造成短路和工作不正常。
根据虚短原理,运算放大器OPAB的反向输入端被钳置在0电平。则由左至右流过RL的电流等于0.1/RL,改写为导纳YL后有:
又由虚短原理,流过电阻R1后,运算放大器OPAB的输出为:
也就是使运算放大器OPAB的输出电压正比于导纳信号发生器输出的阻抗。将(3)、(2)和(4)式代入上式得到下式。
上式中K是打开的固态继电器的数量,ω是正弦变化导纳的角速度,单位为V(伏特)。第一项代表静态导纳,第二项代表动态导纳。与人体导纳相似,动态导纳约为静态导纳的百分之一。此时通过万用表测量OPAB的输出,可以发现每当导纳信号发生器在键盘控制下多打开一个固态继电器开关,输出的直流电压就增加0.5V。
为了进一步验证动态导纳的正确性,需要将上式信号中的第二项(交流部分)放大100倍左右。运算放大器OPAA就是这个交流放大器,Cac是隔直电容,二极管D1、D2起到尽快稳定交流放大器工作点的作用,OPAA接成同相放大形式,以提高输入阻抗。下图是从示波器上观测到的OPAA的输出。
图7 用示波器观测动态导纳变化
与预想的相同,OPAA的输出是一个稍有噪声的正弦信号,该正弦信号频率随导纳信号发生器的设置的改变而改变。图6中的OPAB被连接成了反相放大形式,图7中的负半周对应图5中理想正弦波形的正半周。可以看到图7的实测波形的负半周确实分辨率较正半周低。另外,由于Matlab生成阻值代码表格中的点数只有64点,实测波形的连续性也不十分理想。但对于人体导纳测试应用这已经足够了,若有更高需求,可以适当增加阻值表格的长度。
自制导纳信号发生器 [原创cnblogs.com/helesheng]的更多相关文章
- 用STM32定时器测量信号频率——测频法和测周法[原创cnblogs.com/helesheng]
工业测试与控制系统中,经常需要对未知信号的频率进行测试.对于10MHz以下的信号,用单片机(MCU)定时器完成这项任务显然是最常见和最佳的选择.目前性价比最高的单片机STM32拥有功能强大且数量众多的 ...
- 为树莓派添加一个强实时性前端[原创cnblogs.com/helesheng]
树莓派是最近流行嵌入式平台,其自由的开源特性以及低廉的价格,吸引了来 自全球的大量极客和计算机大咖的关注.来自各大树莓派社区的幕后英雄,无私地在这个开源硬件平台上做了大量的工作,将其打造成了世界上通用 ...
- 国产CPLD(AGM1280)试用记录——做个SPI接口的任意波形DDS [原创www.cnblogs.com/helesheng]
我之前用过的CPLD有Altera公司的MAX和MAX-II系列,主要有两个优点:1.程序存储在片上Flash,上电即行,保密性高.2.CPLD器件规模小,成本和功耗低,时序不收敛情况也不容易出现.缺 ...
- RS485自动收发切换电路 [原创www.cnblogs.com/helesheng]
RS485是最常见的一种远距离可靠传输和组网的UART串口信号接口协议.与同样传输UART串口信号的RS422协议相比,RS485使用半双工通信,即只有一个信道,在同一时刻要么从A到B,要么从B到A传 ...
- STM32的SPI口的DMA读写[原创www.cnblogs.com/helesheng]
SPI是我最常用的接口之一,连接管脚仅为4根:在常见的芯片间通信方式中,速度远优于UART.I2C等其他接口.STM32的SPI口的同步时钟最快可到PCLK的二分之一,单个字节或字的通信时间都在us以 ...
- 给初学者的STM32(Cortex-M3)中断原理及编程方法介绍 [原创www.cnblogs.com/helesheng]
本人编著的<基于STM32的嵌入式系统原理及应用>(ISBN:9787030697974)刚刚在科学出版社出版.这本书花费了半年以上的时间,凝聚了笔者作为高校教师和嵌入式工程师的一些经验, ...
- LabVIEW生成.NET的DLL——C#下调用NI数据采集设备功能的一种方法 [原创www.cnblogs.com/helesheng]
LabVIEW是NI公司的数据采集设备的标准平台,在其上调用NI-DAQmx驱动和接口函数能够高效的开发数据采集和控制程序.但作为一种图形化的开发语言,使用LabVIEW开发涉及算法和流程控制的大型应 ...
- 在STM32F401上移植uC/OS的一个小问题 [原创]
STM32F401xx是意法半导体新推出的Cortex-M4内核的MCU,相较于已经非常流行的STM32F407xx和STM32F427xx等相同内核的MCU而言,其特点是功耗仅为128uA/MHz, ...
- 用树莓派和DS18B20做个汽车温度记录仪[原创]
用树莓派和DS18B20做个汽车温度记录仪[原创] 很想知道夏日阳光暴晒下,汽车内的最高温度以及温度的变化情况.觉得用树莓派和DS18B20来实现应该很简单,于是就尝试捣鼓了一下,半天时间就搞定了,写 ...
随机推荐
- Python3 之 列表推导式
列表推导式(又称列表解析式)提供了一种简明扼要的方法来创建列表. 它的结构是在一个中括号里包含一个表达式,然后是一个for语句,然后是 0 个或多个 for 或者 if 语句.那个表达式可以是任意的, ...
- NTP服务搭建详解一条龙
说在前面:ntp和ntpdate区别 ①两个服务都是centos自带的(centos7中不自带ntp).ntp的安装包名是ntp,ntpdate的安装包是ntpdate.他们并非由一个安装包提供. ② ...
- linux常规网卡配置正确,但是出不了路由的解决方法
netstat -rn #查看是网关 route add default gw 192.168.128.2 dev eth0 # 手动加入网关地址 此类情况容易出现在双网卡配置后
- 预分配——fallocate的前世今生
最近比较懒,还是加班写点东西吧,不然过段时间又把这些整理的东西弄丢了. 写什么呢?写一些跟工作相关的吧!因为笔者从事多媒体录像相关的开发工作,因此常常涉及到优化写卡策略.提升写卡性能相关的方面的事情. ...
- mybatis的@Options的使用
1.问题: 我采用的是mybatis的注解方式,打算插入一条数据之后返回主键,但是试了好几次都是返回的影响的记录数:1, @Insert(****) @Options(useGeneratedKeys ...
- 【数据结构】之串(C语言描述)
串(字符串)是编程中最常用的结构,但 C语言 中没有“字符串”这种变量,只能通过字符数组的形式表示字符串. C语言 为我们提供了一个 string.h 的头文件,通过这个头文件,我们可以实现对字符串的 ...
- CentOS 7 Nginx部署.NET Core Web应用
部署.NET Core运行时 必要前提 在安装.NET Core前,需要注册Microsoft签名秘钥并添加Microsoft产品提要,每台机器只需要注册一次,执行如下命令: sudo rpm -Uv ...
- java this,super简单理解
*****this****** 表示对当前对象的引用. 作用:1.区分实例变量和局部变量(this.name----->实例变量name) 2.将当前对象当做参数传递给其它对象和方法.利用thi ...
- Spring Data JPA 条件查询的关键字
Spring Data JPA 为此提供了一些表达条件查询的关键字,大致如下: And --- 等价于 SQL 中的 and 关键字,比如 findByUsernameAndPassword(Stri ...
- Thinkphp5——数据库表名的大小写问题
ThinkPHP5中数据库的表名如果是驼峰命名法,会被转换成小写加下划线,解决方法如下: 1.表名全部小写,因为数据库的表名区分大小写的. 2.使用Db::table("表名"), ...