Первые 5 минут гугления показали — почти все производители полупроводников, имеют красивые решения, на одном кристалле. Сложные схемы на ОУ, остались далеко в прошлом.
Выбор пал на INA260
Напряжение до 36v, простой для монтажа корпус, компромиссная стоимость.
Но самый решающий аргумент, он уже валялся в тумбе :D Среди прочих образцов.
Настало время его задействовать.
Включаемая схема ничем не отличается от приведенной в даташите.
Ее не высокая сложность, позволяет все собрать на коленке
Современный Current/Power Monitor ток измерит, напряжение отобразит. Если потребуется, даже Alert-ом сообщит о превышении порогового значения!
Для сборки необходимо минимальное количество компонентов (5 R + 2 C).
Можно поискать готовые модули на азиатских площадках, например на INA219.
Тогда вероятно нужна несложная доработка функции пересчета:
Написал класс опроса:
// синглтон для записи логов
static logger& loggerInstance = logger::Instance();
I2cPoll::I2cPoll() {
// создаем класс БД
db = new DbConnection();
if(db->isConnecting()) {
loggerInstance.appendToLog("I2cPoll: db-connected-OK\r\n");
} else {
loggerInstance.appendToLog("I2cPoll: db opening -ERROR\r\n");
}
// структура позволят опрашивать больше одного устройства
// указываем с которого начинать опрос
this->currentDeviceType = startDeviceType;
}
// функция опроса
void I2cPoll::pollExect() {
uint8_t *p_data = NULL;
uint16_t word = 0;
bool res = false;
char cmd_command[128] = {0};
S_insertData device_data;
int device_addr = 0;
// формируем запрос
switch(currentDeviceType) {
case dev_i2c_ina260: {
// извлекаем из БД id устройства
device_addr = db->getDeviceAddrFromName(
i2c_Dev_typeName.ina260.name_text.c_str()
);
// если извлекли правильно
if(device_addr != 0) {
// интересует 3 параметра - ток, напряжение и мощность
for(int i=0; i<3; i++) {
switch(i) {
// curent
case 0:
sprintf(cmd_command, "i2cget -y 0 0x%X 0x01 w\r\n", device_addr);
// отправляем в консоль
res = readWord(cmd_command, &word);
if(res) { // проверяем ответ
p_data = (uint8_t*)&word;
float raw_data;
fprintf(stdout, "Raw u16 %x\r\n", word);
// преобразование по описанию из даташита
raw_data = (0xFF & p_data[1]) | ((0xFF & (p_data[0])) << 8);
device_data.parameter.power_monitor.currnetFlowing =
raw_data * 1.25 / 1000;
fprintf(stdout, "Current %4.2f\r\n",
device_data.parameter.power_monitor.currnetFlowing
);
}
break;
// voltage
case 1:
sprintf(cmd_command, "i2cget -y 0 0x%X 0x02 w", device_addr);
// отправляем в консоль
res = readWord(cmd_command, &word);
if(res) { // проверяем ответ
p_data = (uint8_t*)&word;
float raw_data;
fprintf(stdout, "Raw u16 %x\r\n", word);
// преобразование по описанию из даташита
raw_data = (0xFF & p_data[1]) | ((0xFF & (p_data[0])) << 8);
device_data.parameter.power_monitor.voltage =
raw_data * 1.25 / 1000;
fprintf(stdout, "Volage %4.2f\r\n",
device_data.parameter.power_monitor.voltage
);
}
break;
case 2:
// power
sprintf(cmd_command, "i2cget -y 0 0x%X 0x03 w\r\n", device_addr);
// отправляем в консоль
res = readWord(cmd_command, &word);
if(res) { // проверяем ответ
p_data = (uint8_t*)&word;
float raw_data;
fprintf(stdout, "Raw u16 %x\r\n", word);
// преобразование по описанию из даташита
raw_data = (0xFF & p_data[1]) | ((0xFF & (p_data[0])) << 8);
device_data.parameter.power_monitor.currentPower =
raw_data * 1.25;
fprintf(stdout, "Power %4.2f\r\n",
device_data.parameter.power_monitor.currentPower
);
}
break;
}
}
// если все опросили, отправляем данные в БД
if(res) {
db->insertData(device_data);
}
}
}
break;
default : // если что-то не так, начинаем с первого устройства
currentDeviceType = startDeviceType;
break;
}
// после конца цикла, начинаем с первого устройства
if(currentDeviceType >= (E_I2c_device)endTypeDeviceType) {
currentDeviceType = startDeviceType;
fprintf(stdout, "I2c parce -endDev\r\n");
} else { // иначе опрашиваем следующий
currentDeviceType++;
fprintf(stdout, "I2c parce -nextDev\r\n");
}
}
// формируем запрос на шину i2c
// парсим данные в ответе
bool I2cPoll::readWord(char *cmd_command, uint16_t *p_word) {
int res = false;
int word = 0;
FILE *stream;
// отправляем в консоль
stream = popen(cmd_command, "r");
fprintf(stdout, "cmd_command - %s\r\n", cmd_command);
std::string data;
if (stream) {
char reply_buff[128] = {0};
while (!feof(stream))
if(fgets(reply_buff, sizeof(reply_buff), stream) != NULL) {
data.append(reply_buff);
}
pclose(stream);
}
// проверяем ответ
fprintf(stdout, "shell result :\r\n%s\r\n", data.c_str());
if(data.length() != 0) {
char *p_start = strstr((char*)data.c_str(), "0x");
if(p_start != NULL) {
res = sscanf(p_start, "%x", &word);
if(res) {
*p_word = (uint16_t)word;
}
fprintf(stdout, "getWord %x\r\n", *p_word);
}
}
return res;
}
I2cPoll::~I2cPoll() {
// TODO Auto-generated destructor stub
}
Запускать файл вручную не интересно, это может привести к дублированию процессов.
Правильнее использовать демон. После прочтения поста @shevmax, смысл написания собственной реализации fork отпал. Ограничился небольшим рефакторингом:
// функция которая инициализирует рабочие потоки
int initWorkThread() {
loggerInstance.appendToLog("[DAEMON] Init...\r\n");
i2c_poller = new I2cPoll();
std::thread thr(thread_proc);
threads.emplace_back(std::move(thr));
loggerInstance.appendToLog("[DAEMON] Init -Ok\r\n");
return 0;
}
void thread_proc(void) {
for(;;) {
{
std::lock_guard<std::mutex> lock(mtx_thread_poll);
i2c_poller->pollExect();
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
После добавления в проект fork, нужен скрипт init.d
Сохраним его как /etc/init.d/i2c_poller.sh
#!/bin/sh
dir="/home/khomin/Project/i2c_poller/bin/"
cmd="/home/khomin/Project/i2c_poller/bin/i2c_poller"
user="root"
name="i2c_poller"
pid_file="/var/run/$name.pid"
log_dir="/var/log/i2c_poller/"
stdout_log="$log_dir/$name.log"
stderr_log="$log_dir/$name.err"
get_pid() {
cat "$pid_file"
}
is_running() {
[ -f "$pid_file" ] && ps -p `get_pid` > /dev/null 2>&1
}
case "$1" in
start)
if is_running; then
echo "Already started"
else
echo "Starting $name"
rm -rf $log_dir
mkdir $log_dir
cd "$dir"
if [ -z "$user" ]; then
sudo $cmd >> "$stdout_log" 2>> "$stderr_log" &
else
sudo -u "$user" $cmd >> "$stdout_log" 2>> "$stderr_log" &
fi
echo $! > "$pid_file"
if ! is_running; then
echo "Unable to start, see $stdout_log and $stderr_log"
exit 1
fi
fi
;;
stop)
if is_running; then
echo -n "Stopping $name.."
kill `get_pid`
for i in 1 2 3 4 5 6 7 8 9 10
# for i in `seq 10`
do
if ! is_running; then
break
fi
echo -n "."
sleep 1
done
echo
if is_running; then
echo "Not stopped; may still be shutting down or shutdown may have failed"
exit 1
else
echo "Stopped"
if [ -f "$pid_file" ]; then
rm "$pid_file"
fi
fi
else
echo "Not running"
fi
;;
restart)
$0 stop
if is_running; then
echo "Unable to stop, will not attempt to start"
exit 1
fi
$0 start
;;
status)
if is_running; then
echo "Running"
else
echo "Stopped"
exit 1
fi
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
exit 0
Проверим, что запускается
После выполнения /etc/init.d/i2c_poller.sh start
Тут же видим данные в базе, значит опрос работает
Проект на git
TODO: использование i2cget — не самое рациональное решение. В зависимости от типа одноплатника, есть смысл включить драйвер i2c при сборке ядра.
Комментарии (18)
shekelgruber
11.06.2018 14:28+31. Зачем класть в базу JSON?
2. Опрос датчика происходит раз в секунду? В сутках 86400 секунд, две недели — миллион записей, год — 26 миллионов. База осилит?
Хранить временные ряды в реляционной БД — не самое правильное решение, особенно вот таким способом.
blog.timescale.com/time-series-data-why-and-how-to-use-a-relational-database-instead-of-nosql-d0cd6975e87c
luca.ntop.org/tsdb.pdf
Armleo
12.06.2018 06:41+1Что за говно код! Вы бы хотя бы отформатировали код, у вас в каждом втором слове опечатка. Где-то верблюд, где-то нижнее тире.
Зачем класть в базу столько лишней информации?
Armleo
12.06.2018 06:44-1std::lock_guard<std::mutex> lock(mtx_thread_poll);
i2c_poller->pollExect();
Это у вас в main.c такой? Какая версия C? C45?
olartamonov
12.06.2018 13:18Первые 5 минут гугления показали — почти все производители полупроводников, имеют красивые решения, на одном кристалле
«Любая проблема имеет простое, понятное и очевидное неправильное решение»
Современная цифровая электроника может легко менять своё потребление в десятки раз на миллисекундных интервалах — и системы с сэмплированием тока такие пики или пропускают, выдавая в качестве результата среднее по больнице, или генерируют адские объёмы данных. Единственный случай, когда они разумны и адекватны — если вы знаете, что у вашей нагрузки таких коротких пиков и/или провалов не бывает.
В остальных случаях нужно что-то типа Ti EnergyTrace, который отслеживает режим работы DC/DC, питающего нагрузку, и по его коэффициенту заполнения считает, сколько энергии в эту нагрузку было влито.olartamonov
12.06.2018 13:28NB: EnergyTrace имеется в виду старый, ламповый: www.ti.com/tool/ENERGYTRACE
In debuggers that support EnergyTrace™ technology, a software-controlled DC-DC converter generates the target power supply. The time density of the DC-DC converter charge pulses equals the energy consumption of the target microcontroller. A built-in calibration circuit in the debug tool defines the energy equivalent for a single charge pulse. Since the width of each charge pulse remains constant, the debugger can just count every charge pulse and then sum them over time to calculate an average current – which leads to very accurate measurements. Using this approach, even the shortest device activity that consumes energy contributes to the overall recorded energy.
В новом LaunchPad CC1312R формально тоже EnergyTrace, но уже более тривиальный, на шунте и быстро молотящем АЦП (см. выше про объёмы данных; с другой стороны, всегда же можно сырые данные с их 100 кГц sample rate числомолотить сразу и хранить усреднённые).
beerware
12.06.2018 16:37В данном решении пик просто не дойдет до места назначения, благодаря конденсатору 0.1мкФ. Постоянная времени RC цепочки подразумевает оцифровку со скоростью не лучше 1 выборка/сек
olartamonov
12.06.2018 19:30Не хочу вас расстраивать, но RC-цепочка 2 ? 10 Ом ? 0,1 мкФ подразумевает оцифровку со скоростью на 6 (шесть) порядков выше.
Вообще для того, чтобы сколь-нибудь адекватно оценить реальное потребление встраиваемой системы в общем случае методом сэмплирования тока на шунте, надо как минимум 10 kSPS скорости и 10^4 — 10^5 динамического диапазона.
А представленное в посте годится только в радиокружок, в группу младшего возраста.
ittakir
12.06.2018 20:09Электролитические конденсаторы в сотни мкФ, которые стоят в обвязке питания линуксового компьютера сглаживают эти пики.
olartamonov
12.06.2018 20:16Не хочу вас расстраивать, но помимо того, что в статье подобным образом сфера применимости устройства не очерчена, даже для линуксового компьютера измерять потребление раз в секунду и делать из этого какие-то выводы — очень плохая идея.
win32asm
12.06.2018 20:46для демона более правильно использовать не fork() а daemon()
см. http://man7.org/linux/man-pages/man3/daemon.3.html
ZigFisher
12.06.2018 22:27Несколько лет назад выложил у себя на GitHub подборку утилит i2c-telemetry для OpenWrt роутеров.
Буду рад, если кому-то пригодится.
j_wayne
Спасибо за наводку. А упрощенную схему подключения не могли бы добавить, хотя бы от руки нарисованную? Та, что из даташита приложена, с ходу не очень понятна.
Khomin Автор
От руки, будет 1:1 как в даташите))
j_wayne
ИМХО там много несущественных деталей. Особенно с мобилки и на бегу как-то не наглядно.