自制导纳信号发生器 [原创cnblogs.com/helesheng]
最近正在研制一种通过测量人体导纳,估算体内血液变化率,进而评估心血管系统泵血功能的医疗仪器。为测量人体导纳,我们设计了一套巧妙的激励信号幅度反馈电路,该电路由于涉及商业机密就不在这里讨论了。这里主要分享一下我自己设计的,用于对导纳测量电路进行调试和幅度定标的重要工具——导纳信号发生器的设计。
以下原创内容欢迎网友转载,但请注明出处:http://cnblogs.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算法的实现代码如下。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void OSTimeTickHook (void) 2 { 3 unsigned char MCP41XXX_1st_byte,MCP41XXX_2nd_byte;//需要通过SPI口发送给MCP41XXXX的两个字节 4 MCP41XXX_1st_byte = 0X11;//对电位器1执行写数据操作 5 MCP41XXX_2nd_byte = 0x00;//第二个字节是需要写入的电阻数值 6 /////以下DDS算法产生频率可调节的正弦导纳值//////////// 7 unsigned short delta;//DDS算法的地址表增量 8 delta = admit_frq*65536/1000;//根据DDS算法,地址表增量等于目标频率admit_frq乘以表格长度L,再除以采样率fs 9 dds_adder = dds_adder + delta;//累加器增加并自然溢出 10 MCP41XXX_2nd_byte = 256 - sin_admit_tbl[dds_adder>>10];//数值表格只有64个数,也就是6位地址可以覆盖 11 //最后用256减去查表的值是因为电路图画的有问题:输入MCP41XXX的数值是PW和PB之间的值, 12 //但电路图将MCP41XXX连接成变阻器的方法是将PW和PB短路,从而外部得到的是PW和PA的值,所以将数值反转后才能得到正确的值 13 CS_RP = 0; 14 SPIx_ReadWriteByte(MCP41XXX_1st_byte); 15 SPIx_ReadWriteByte(MCP41XXX_2nd_byte); 16 CS_RP = 1; 17 }
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();负责定时地、逐位地刷新数码管上显示的内容,以及静态导纳电路中需要打开的固态继电器。
键盘任务代码如下所示。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void TaskKEY(void *pdata) 2 { 3 while(1) 4 { 5 OSTimeDly(20); 6 if(KEY1 == 0) 7 { 8 OSTimeDly(20);//消除按键抖动 9 if(KEY1 == 0) 10 { 11 if(admit_frq < MAX_FRQ) 12 admit_frq++; 13 while(KEY1 == 0)OSTimeDly(10);//一直等待到按键被释放,最后的延时还能消抖动 14 } 15 } 16 if(KEY2 == 0) 17 { 18 OSTimeDly(20);//消除按键抖动 19 if(KEY2 == 0) 20 { 21 if(admit_frq > MIN_FRQ) 22 admit_frq--; 23 while(KEY2 == 0)OSTimeDly(10);//一直等待到按键被释放,最后的延时还能消抖动 24 } 25 } 26 if(KEY3 == 0) 27 { 28 OSTimeDly(20);//消除按键抖动 29 if(KEY3 == 0) 30 { 31 if(r_sw_num < MAX_SW) 32 r_sw_num++; 33 while(KEY3 == 0)OSTimeDly(10);//一直等待到按键被释放,最后的延时还能消抖动 34 } 35 } 36 if(KEY4 == 0) 37 { 38 OSTimeDly(20);//消除按键抖动 39 if(KEY4 == 0) 40 { 41 if(r_sw_num > 0) 42 r_sw_num--; 43 while(KEY4 == 0)OSTimeDly(10);//一直等待到按键被释放,最后的延时还能消抖动 44 } 45 } 46 } 47 }
刷新显示和输出任务代码如下所示。其中显示缓存dis_buff[]中对应的是需要显示的每个数码管位的内容。dis_buff[]的内容需要不断计算、刷新,以防键盘任务在用户操作时修改需要显示的值。变量first_byte,second_byte,third_byte中的值则是需要通过串行口下载到三只74HC595中的。其中既包括当前需要显示的数码管位的字形码D0-D7,也包括显示的位置选通信号DIG0-DIG3和固态继电器的开关信号SWs1- SWs10,其对应关系请参见图3中电路网络标号。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 void TaskFLASH_DIS(void *pdata) 2 { 3 unsigned char dis_buff[4],i; 4 unsigned short sw_ctl_bits=0;//低10位对应100欧姆导纳电阻的开关状态 5 unsigned char dis_index = 0;//刷新数码管到第几位的计数器 6 unsigned char bit_sel = 1;//决定哪位被选中显示,必须有一个位为1 7 unsigned char first_byte,second_byte,third_byte; 8 while(1) 9 { 10 //////////先刷新需要595输出的所有东西的数值/////////// 11 dis_buff[3] = r_sw_num/10;//数码管的右半边两个显示打开的100欧姆电阻数目 12 dis_buff[2] = r_sw_num%10; 13 if(dis_buff[3] == 0)//处理十位的消隐问题 14 dis_buff[3] = LED_PATT[10];//消隐显示 15 else 16 dis_buff[3] = LED_PATT[dis_buff[3]];//查字型表 17 dis_buff[2] = LED_PATT[dis_buff[2]];//查字型表 18 dis_buff[1] = admit_frq/10;//数码管的左半边两个显示导纳变化的频率 19 dis_buff[0] = admit_frq%10; 20 if(dis_buff[1] == 0)//处理十位的消隐问题 21 dis_buff[1] = LED_PATT[10];//消隐显示 22 else 23 dis_buff[1] = LED_PATT[dis_buff[1]];//查字型表 24 dis_buff[0] = LED_PATT[dis_buff[0]];//查字型表 25 //由打开的100欧姆电阻数r_sw_num,决定打开的位 26 sw_ctl_bits = 0; 27 if(r_sw_num != 0) 28 { 29 for(i = 0;i < r_sw_num;i++) 30 { 31 sw_ctl_bits = sw_ctl_bits<<1; 32 sw_ctl_bits++;//将最低位置一,并左移一位 33 } 34 } 35 bit_sel = 0x01 << dis_index;//刷新到哪一位,就让对应的位选信号为1 36 //////////刷新595输出,每次让一个数码管亮/////////// 37 third_byte = dis_buff[dis_index];//第三个送出的字节是字形码,但具体是那个位的,要由当前扫描到的位置决定 38 ///////第二个字节的最高两个位控制100欧姆导纳电阻的最低两个开关,第四位是数码管的位选通信号 39 second_byte = (sw_ctl_bits & 0x0003);//第二个字节中只去最低两个位 40 second_byte = second_byte << 6;//将最低两个位放到最高两个位置 41 second_byte = second_byte + bit_sel;//第四位是位选通信号 42 first_byte = (sw_ctl_bits >> 2) & 0x00ff;//取开关控制位的3-10位 43 send_to_tri_74595(first_byte,second_byte,third_byte);//从模拟SPI口送出三个准备好的字节 44 if(dis_index < 3)//更新下一个需要刷新的位置 45 dis_index++; 46 else 47 dis_index = 0; 48 OSTimeDly(5);//刷新速度是5个时钟周期也就是200Hz 49 } 50 }
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点,实测波形的连续性也不十分理想。但对于人体导纳测试应用这已经足够了,若有更高需求,可以适当增加阻值表格的长度。