Power monitor для встраиваемых систем (Linux)

в 10:57, , рубрики: c++, fork, linux, postgresql, Разработка под Linux

Потребовалось измерить ток потребления одного устройства
+ хранить полученные значения в таблице БД (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-OKrn");
	} else {
		loggerInstance.appendToLog("I2cPoll: db opening -ERRORrn");
	}
// структура позволят опрашивать больше одного устройства
// указываем с которого начинать опрос
	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 wrn", device_addr);
					// отправляем в консоль
					res = readWord(cmd_command, &word);
					if(res) {  // проверяем ответ
						p_data = (uint8_t*)&word;
						float raw_data;
						fprintf(stdout, "Raw u16 %xrn", 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.2frn", 
                                                     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 %xrn", 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.2frn", 
                                                     device_data.parameter.power_monitor.voltage
                                                );
					}
					break;

				case 2:
					//  power
					sprintf(cmd_command, "i2cget -y 0 0x%X 0x03 wrn", device_addr);
					// отправляем в консоль
					res = readWord(cmd_command, &word);
					if(res) {  // проверяем ответ
						p_data = (uint8_t*)&word;
						float raw_data;
						fprintf(stdout, "Raw u16 %xrn", 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.2frn", 
                                                    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 -endDevrn");
	} else { // иначе опрашиваем следующий
		currentDeviceType++;
		fprintf(stdout, "I2c parce -nextDevrn");
	}
}

// формируем запрос на шину 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 - %srn", 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 :rn%srn", 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 %xrn", *p_word);
		}
	}
	return res;
}

I2cPoll::~I2cPoll() {
	// TODO Auto-generated destructor stub
}

Запускать файл вручную не интересно, это может привести к дублированию процессов
Правильнее использовать демон
После прочтения поста @shevmax, смысл написания собственной реализации fork отпал
Ограничился небольшим рефакторингом

Создаем поток и помещаем в него i2cPoller (часть main.c)

// функция которая инициализирует рабочие потоки
int initWorkThread() {
	loggerInstance.appendToLog("[DAEMON] Init...rn");

	i2c_poller = new I2cPoll();

	std::thread thr(thread_proc);
	threads.emplace_back(std::move(thr));

	loggerInstance.appendToLog("[DAEMON] Init -Okrn");
	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
Power monitor для встраиваемых систем (Linux) - 3

Тут же видим данные в базе, значит опрос работает
Power monitor для встраиваемых систем (Linux) - 4

Проект на git
https://github.com/khomin/i2c_poller

TODO: использование i2cget — не самое рациональное решение
В зависимости от типа одноплатника, есть смысл включить драйвер i2c при сборке ядра

Автор: Khomin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js