之前使用INA226的文章主要是通过PICO反复的轮询,获取INA226的采集数据,但是INA226采集数据的间隔时间其实是比较长的,理想的情况是INA226完成采集之后,通过中断通知PICO去获取数据。INA226也的确支持这个方式。
翻看INA226的datasheet,可以发现相关的寄存器是06H和07H,06H主要是用于设定alter引脚的功能。06H的位功能如下:
struct
{
bool alter_latch_enable : 1; // 设置Alter的锁定模式,0不锁存(缺省,故障恢复后自动清除),1锁存(不会自动清除,直到读取 REGISTER_MASK_ENABLE 寄存器之后);
bool alter_polarity_bit : 1; // 设置Alter Pin的极性,0缺省(触发时拉低电平),1反转;
bool math_overflow_flag : 1; // 计算溢出,该标记被设置为1时,预示着电流和功率数据可能无效;(读取)
bool conversion_ready_flag : 1; // 当Alter Pin触发时,该标志标记来源是采样完成;(读取)
bool alter_function_flag : 1; // 当Alter Pin触发时,该标志标记来源是警告;(读取)
bool reserved_1 : 1;
bool reserved_2 : 1;
bool reserved_3 : 1;
bool reserved_4 : 1;
bool reserved_5 : 1;
bool conversion_ready : 1; // 触发Alter Pin,当采样完成
bool power_over_limit : 1; // 触发Alter Pin,当功率超过阈值
bool bus_voltage_under_voltage : 1; // 触发Alter Pin,当总线电压低于阈值
bool bus_voltage_over_voltage : 1; // 触发Alter Pin,当总线电压超过阈值
bool shunt_voltage_under_voltage : 1; // 触发Alter Pin,当分流器电压低于阈值
bool shunt_voltage_over_voltage : 1; // 触发Alter Pin,当分流器电压超过阈值
} mask;
这个是从0位到15位的定义,PICO使用的是小端模式,因此定义是从低端到高端的位定义。
另外,07H寄存器内,放置的是警报的限制数据。
The Alert Limit Register contains the value used to compare to the register selected in the Mask/Enable Register to determine if a limit has been exceeded.
07H寄存器内放的数据是整形,因此,07H内放的数据是采样转换成浮点数之前的数据。我们在设定07H的时候,必须从浮点数转换成整数。方法如下
// Shunt voltage
// lsb 2.5uV, 0.000,0025
int16_t value = (int16_t)( upper / 0.0000025 );
// Bus Voltage
// lsb 1.25mV, 0.00125
int16_t value = (int16_t)( upper / 0.00125 );
// Power
// lsb 25mW, 0.025
uint16_t value = (uint16_t)( upper / 0.025 );
关于06H的设定上,必须有一个说明,就是报警位11-15位,不能多重设定,因为07H只能保存一个数据,因此,只能在5个警报方式内五选一,如果要改变设定,就必须把之前的设定位清除掉。
另外,当Alter pin被触发的时候,06H内也存储了触发的原因,包括三种:采样完成(设定了采样通知),警报(设定了警报),数据异常。我们可以通过这几个标志来确定Alter pin被触发的理由。Pico在中断之后,必须读取06H,获取到引发中断的原因,并根据条件继续做处理。
06H有一个比较重要的位,01位,是否反转Alter Pin的极性。Alter Pin是开漏模式,正常情况,PICO看来Alter Pin是高位,当触发警报的时候,Alter Pin会拉底Pico GPIO的电压并导通。所以,如果有特殊要求,可以改变这个值达到目的。
硬件的设计如下:
GPIO1,2通过上拉电阻接到INA226的数据端口,GPIO则通过电阻+LED接到Alter Pin。 这样,当Alter 开启的时候,LED就会发光提示。
硬件准备好,我们接下来就开始说一下如何实现PICO的中断部分。
GPIO的中断实现非常的简单,代码如下:
gpio_set_function( gpio, GPIO_FUNC_SIO );
gpio_pull_up( gpio );
gpio_set_irq_enabled_with_callback( gpio, event, true, & PicoInterruptEntry );
三条语句就能开启中断。而event则监控下拉事件,因为INA226的Alter Pin是开漏,因此,必须先把GPIO拉高,稍微麻烦点就是要设定一个回调函数入口,但是有一点必须提醒的是,中断仅仅是中断,仅仅用于处理数据接收,不能用来做非常复杂的事情,这种情况非常类似于多线程环境下的编程,因此,数据必须有临界区的保护,在中断程序处理了数据采集之后,退出临界区,并通过信号量通知主循环来处理数据。也就是说,所有重度操作,都必须放在主循环之内,而不是让中断来处理。中断结束的越快越好。
上面介绍了大致的框架,接下来,我简单的把关键的代码贴上来:
void CPicoFilamental::Entry(uint32_t events)
{
bool awaken(false);
if( GPIO_IRQ_EDGE_FALL & events || GPIO_IRQ_LEVEL_LOW & events )
{
section.Enter();
counter ++;
bool alter_function(false), conversion_ready(false), math_overflow(false);
if( sampler.GetFlags(alter_function, conversion_ready, math_overflow) )
{
//Serial.printf( "flags...%d, %d, %d\n", alter_function, conversion_ready, math_overflow );
}
if( GPIO_IRQ_EDGE_FALL & events )
{
if( conversion_ready && !math_overflow )
{
f_current = sampler.GetCurrent();
awaken = true;
//Serial.printf( "current: %f\n", f_current );
}
}
else
if( GPIO_IRQ_LEVEL_LOW & events )
{
if( alter_function )
{
//Serial.printf( "alter error\n" );
awaken = true;
}
}
section.Leave();
if( awaken ) {
signal.Awake();
}
}
}
上面这一段是中断的处理程序,非常的简单,就是采集数据,根据结果简单的处理,然后通知主循环来干活。
bool CPicoFilamental::Run()
{
double voltage_percent = 0.0;
for( ;; )
{
section.Enter();
double diff = abs( f_current - e_current );
diff = diff / e_current;
// 误差 1% 以内不执行任何操作
if( diff > 0.01 )
{
double result = pid.StepCalculate( e_current, f_current ) * 0.0033;
voltage_percent += result;
voltage_percent = min( voltage_percent, 0.08 );
voltage_percent = max( voltage_percent, 0.0 );
pwm_1.Adjust( voltage_percent );
pwm_2.Adjust( voltage_percent );
Serial.printf( "feedback: %f, percent: %f, i:%f\n", f_current, voltage_percent, pid.integral );
}
// Convert to percent
section.Leave();
signal.Await( 2000 );
signal.Reset();
}
return true;
}
上面这一段就是主循环的处理,大概就是通过电流值,用PID算法计算下一步的PWM占空比。然后输出PWM。重点是信号量的使用。演示了中断如何通过信号量来控制主循环。
#include "Signal.h"
CSignal::CSignal()
{
sem_init( & signal, 0, 1 );
}
CSignal::~CSignal()
{
}
bool CSignal::Reset()
{
sem_reset( & signal, 0 );
return true;
}
bool CSignal::Awake()
{
sem_reset( & signal, 1 );
return true;
}
bool CSignal::Await(uint32_t milisecond)
{
return sem_acquire_timeout_ms( & signal, milisecond );
}
上面信号量的代码其实也非常的简单。也是对PICO SDK的一个简单封装。
最后,必须要提一句,使用中断的方式,最好是采用触发模式,而不是使用连续模式。我在调试中发现,TI的芯片,如果采用连续模式,很有可能一段时间之后,读出来的数据是错的,Shunt Voltage的值全是FFFE之类的补码,我猜测是芯片本身的一些设计缺陷。采用触发模式,仅需要在所有的读取完成之后,重新触发一次采集即可(就是重新写入00H寄存器)。