Потребовалось измерить ток потребления одного устройства + хранить полученные значения в таблице БД (PostgreSQL).

Первые 5 минут гугления показали — почти все производители полупроводников, имеют красивые решения, на одном кристалле. Сложные схемы на ОУ, остались далеко в прошлом.

Выбор пал на INA260
Напряжение до 36v, простой для монтажа корпус, компромиссная стоимость.
Но самый решающий аргумент, он уже валялся в тумбе :D Среди прочих образцов.
Настало время его задействовать.

Включаемая схема ничем не отличается от приведенной в даташите.
Ее не высокая сложность, позволяет все собрать на коленке

image

Современный Current/Power Monitor ток измерит, напряжение отобразит. Если потребуется, даже Alert-ом сообщит о превышении порогового значения!

Для сборки необходимо минимальное количество компонентов (5 R + 2 C).
Можно поискать готовые модули на азиатских площадках, например на INA219.
Тогда вероятно нужна несложная доработка функции пересчета:

image Написал класс опроса:

I2cPoll
// синглтон для записи логов
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 отпал. Ограничился небольшим рефакторингом:

Создаем поток и помещаем в него i2cPoller (часть main.c)
// функция которая инициализирует рабочие потоки
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)


  1. j_wayne
    11.06.2018 14:04

    Спасибо за наводку. А упрощенную схему подключения не могли бы добавить, хотя бы от руки нарисованную? Та, что из даташита приложена, с ходу не очень понятна.


    1. Khomin Автор
      12.06.2018 19:21

      От руки, будет 1:1 как в даташите))


      1. j_wayne
        12.06.2018 19:47

        ИМХО там много несущественных деталей. Особенно с мобилки и на бегу как-то не наглядно.


  1. shekelgruber
    11.06.2018 14:28
    +3

    1. Зачем класть в базу 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


    1. kAIST
      12.06.2018 00:00

      Справедливое замечание. Почему этот комментарий минусуют?


  1. Armleo
    12.06.2018 06:41
    +1

    Что за говно код! Вы бы хотя бы отформатировали код, у вас в каждом втором слове опечатка. Где-то верблюд, где-то нижнее тире.


    Зачем класть в базу столько лишней информации?


    1. Armleo
      12.06.2018 06:44
      -1

      std::lock_guard<std::mutex> lock(mtx_thread_poll);
      i2c_poller->pollExect();


      Это у вас в main.c такой? Какая версия C? C45?


    1. Armleo
      12.06.2018 06:45
      +1

      Чем вам systemd не угодил? Зачем вообще плодить процессы?!


  1. mazy
    12.06.2018 07:56

    А процессу от рута обязательно?
    Или доступ к i2c требует привелегий?


  1. Zuy
    12.06.2018 08:57

    А зачем тут i2cget вообще нужен? Почему бы напрямую с i2c шиной не поработать?


  1. olartamonov
    12.06.2018 13:18

    Первые 5 минут гугления показали — почти все производители полупроводников, имеют красивые решения, на одном кристалле


    «Любая проблема имеет простое, понятное и очевидное неправильное решение»

    Современная цифровая электроника может легко менять своё потребление в десятки раз на миллисекундных интервалах — и системы с сэмплированием тока такие пики или пропускают, выдавая в качестве результата среднее по больнице, или генерируют адские объёмы данных. Единственный случай, когда они разумны и адекватны — если вы знаете, что у вашей нагрузки таких коротких пиков и/или провалов не бывает.

    В остальных случаях нужно что-то типа Ti EnergyTrace, который отслеживает режим работы DC/DC, питающего нагрузку, и по его коэффициенту заполнения считает, сколько энергии в эту нагрузку было влито.


    1. olartamonov
      12.06.2018 13:28

      NB: 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 числомолотить сразу и хранить усреднённые).


    1. beerware
      12.06.2018 16:37

      В данном решении пик просто не дойдет до места назначения, благодаря конденсатору 0.1мкФ. Постоянная времени RC цепочки подразумевает оцифровку со скоростью не лучше 1 выборка/сек


      1. olartamonov
        12.06.2018 19:30

        Не хочу вас расстраивать, но RC-цепочка 2 ? 10 Ом ? 0,1 мкФ подразумевает оцифровку со скоростью на 6 (шесть) порядков выше.

        Вообще для того, чтобы сколь-нибудь адекватно оценить реальное потребление встраиваемой системы в общем случае методом сэмплирования тока на шунте, надо как минимум 10 kSPS скорости и 10^4 — 10^5 динамического диапазона.

        А представленное в посте годится только в радиокружок, в группу младшего возраста.


    1. ittakir
      12.06.2018 20:09

      Электролитические конденсаторы в сотни мкФ, которые стоят в обвязке питания линуксового компьютера сглаживают эти пики.


      1. olartamonov
        12.06.2018 20:16

        Не хочу вас расстраивать, но помимо того, что в статье подобным образом сфера применимости устройства не очерчена, даже для линуксового компьютера измерять потребление раз в секунду и делать из этого какие-то выводы — очень плохая идея.


  1. win32asm
    12.06.2018 20:46

    для демона более правильно использовать не fork() а daemon()
    см. http://man7.org/linux/man-pages/man3/daemon.3.html


  1. ZigFisher
    12.06.2018 22:27

    Несколько лет назад выложил у себя на GitHub подборку утилит i2c-telemetry для OpenWrt роутеров.
    Буду рад, если кому-то пригодится.