1 Востаннє редагувалося MCS-51 (05.02.2021 10:24:12)

Тема: Допоможіть написати краще

Доброго часу доби. Вивчаю C у контексті embedded.
Допоможіть краще написати шматок коду, який виконує наступну функцію:
У змінній uint8_t знаходиться стан 8 виходів: встановлений біт - увімкнений вихід, скинутий біт - вимкнений вихід.
Необхідно сформувати XML-тег із переліком увімкнених виходів.
Якщо немає увімкнених виходів - сформувати "закритий" тег.

Моє вирішення задачі:

Прихований текст
void getStateOuts(char* stateOutsStr)
{
  // max string: <outs_on>1,2,3,4,5,6,7,8,</outs_on>

  if (stateOuts != 0) {
    strcpy(stateOutsStr, "<outs_on>");
    uint8_t i = 0, j =0;
    for ( ; i < OUTS_NUM; i++)
    {
      if ((stateOuts && 0x01) == 0x01) {
        stateOutsStr[sizeof("<outs_on>")-1-(j*2)+(i*2)] = i+0x31;
        stateOutsStr[sizeof("<outs_on>")-1-(j*2)+(i*2)+1] = ',';
      }
      else {
        j++; // щоб "повернути" вказівник назад у разі вимкненого виходу
      }
      stateOuts >>= stateOuts;
    }
    strcpy(stateOutsStr+(sizeof("<outs_on>")-1-(j*2)+(i*2)+2), "</outs_on>");
  }
  else {
    strcpy(stateOutsStr, "<outs_on/>");
  }
}

2 Востаннє редагувалося koala (05.02.2021 21:30:19)

Re: Допоможіть написати краще

Щось ви сильно не те робите.

MCS-51 написав:

У змінній uint8_t знаходиться стан

У вас функція приймає посилання на char і все. Ви про stateOuts? Передавайте її в функцію окремим параметром.
stateOutsStr мусить посилатися на буфер достатнього розміру - сподіваюся, це ви розумієте.
Вам потрібні такі змінні:
- поточний стан вхідної змінної (наразі ви змінюєте глобальну, це погано);
- поточний номер входу (в циклі);
- посилання на поточний кінець стрічки.
sprintf чи хоча б itoa використовувати платформа не дозволяє?
кома після останнього числа прийнятна?

const char OUTS_ON[] = "<outs_on>";
const char OUTS_ON_CLOSED[] = "<outs_on/>";
const char OUTS_ON_CLOSING[] = "</outs_on>";

void get_state_outs(uint8_t state_outs, char* state_outs_str)
{
  // max string: <outs_on>1,2,3,4,5,6,7,8,</outs_on>
  if(state_outs == 0) //особливу ситуацію обробяємо на початку, щоб далі можна було рівень знизити
  {
    strcpy(state_outs_str, OUTS_ON_CLOSED);
    return;
  }
  
  char *target = state_outs_str;
  strcpy(target, OUTS_ON);
  target += sizeof(OUTS_ON)-1; //OUTS_ON має в кінці \0, але нам він не потрібен
  
  for( uint8_t i=1, bits = state_outs; bits!=0; ++i, bits>>=1) //якщо платформа не дозволяє - винесіть проголошення i з циклу
  {
    if(bits&0x1)
    {
      *target++ = '0'+i; //0x31 - абсолютно незрозуміло, про що мова
      *target++ = ',';
    }
  }
  strcpy(target, OUTS_ON_CLOSING);
}

Пофіксив пару проблем, ось воно онлайн.

Подякували: MCS-511

3 Востаннє редагувалося MCS-51 (06.02.2021 08:02:07)

Re: Допоможіть написати краще

koala написав:

Щось ви сильно не те робите.

Цим і прекрасний процес навчання. Можна дозволити собі робити помилки, і вчитися на них.

koala написав:

sprintf чи хоча б itoa використовувати платформа не дозволяє?
кома після останнього числа прийнятна?

Дозволяє, але там, де можна без них легко обійтися (один символ перетворити тощо), я їх не юзаю.
Кома у кінці прийнятна.

Дякую за допомогу.
Ось мій фінальний варіант:

Прихований текст
const char  OUTS_ON[] =                 "<outs_on>";
const char  OUTS_ON_CLOSING[] =         "</outs_on>";
const char  OUTS_ON_CLOSED[] =          "<outs_on/>";

uint8_t   stateOuts = 0b10101010;
char      stateOutsStr[sizeof("<outs_on>1,2,3,4,5,6,7,8,</outs_on>")] = {0};

void getStateOuts(uint8_t stateOuts, char* stateOutsStr)
{
  if (stateOuts == 0) {
    strcpy(stateOutsStr, OUTS_ON_CLOSED);
    return;
  }
  
  strcpy(stateOutsStr, OUTS_ON);
  stateOutsStr += sizeof(OUTS_ON)-1;
  
  for (uint8_t i=0; stateOuts!=0; ++i, stateOuts>>=1) {
    if (stateOuts & 0x01) {
      *stateOutsStr++ = '1'+i;
      *stateOutsStr++ = ',';
    }
  }
  strcpy(stateOutsStr, OUTS_ON_CLOSING);
}

Для stateOuts = 0b10101010 виводить <outs_on>2,4,6,8,</outs_on>

4 Востаннє редагувалося koala (06.02.2021 16:56:30)

Re: Допоможіть написати краще

Змінювати параметри всередині функції - загалом не дуже добре.
Оскільки ви використовуєте глобальну змінну для виведення - можете це дещо унормувати.

static char      stateOutsStrBuffer[sizeof("<outs_on>1,2,3,4,5,6,7,8,</outs_on>")] = {0};

const char *getStateOutsBuffer(uint8_t stateOuts, char* stateOutsStr)//наша функція
{
  if(...){
    return OUTS_ON_CLOSED;//тут нічого не копіюємо
  }
  ...
  return stateOutsStr;
}

const char *getStateOuts(uint8_t stateOuts)
{
    return getStateOutsBuffer(uint8_t stateOuts, char* stateOutsStrBuffer);
}

Тепер можна писати

printf("%s", getStateOuts(0x55));

або задавати власний буфер, якщо треба

char buffer1[100], buffer2[100];
printf("%s %s", getStateOutsBuffer(0x55, buffer1), getStateOutsBuffer(0xAA, buffer2));

Звісно, двічі в одному виразі можна лише через власні буфери.

Подякували: MCS-511

5

Re: Допоможіть написати краще

Прибрати хвостову кому можна простим

for (uint8_t i=0; stateOuts!=0; ++i, stateOuts>>=1) {
    if (stateOuts & 0x01) {
        *stateOutsStr++ = '1'+i;
        *stateOutsStr++ = ',';
    }
}
*--stateOutsStr = '\0'; // гарантовано хоч одна цифра і кома є, бо вище вихід по stateOuts == 0
strcpy(stateOutsStr, OUTS_ON_CLOSING);

Друк власне переліку номерів встановлених бітів я б виділив в окрему функцію, бо воно може знадобитися ще десь.
А тег додавати навколо її виклику, тоді можна ще й різний тег додавати, передавши його як параметр const char *tag_name.
Наприклад, тег outs_off, а на друк бітів передавати інверсію змінної.

Подякували: MCS-511

6 Востаннє редагувалося ReAl (06.02.2021 19:16:24)

Re: Допоможіть написати краще

Щодо [s[n]]printf

Koala, воно загалом (відносно) жруче до пам'яті, навіть мінімальний варіант без підтримки floating point форматів
та моїх улюблених можливостей на зразок «точності» для рядків ( "%.8s", або  "%.*s" і взагалі * у форматах) може зжерти два кілобайти флеша. Також просить трохи більше стеку під буфер перетворення чисел у текст, деякі інші проблеми. Тому після звички вбирати програму у 8 кілобайт (наприклад, UV EPROM 27C64) жаба давить лінкувати у проект компанію printf.

Але.
MCS-51, я теж раніше старався обійтися без нього. На початку роботи на 8-бітниках. Але десь після ATmega64 (та це ж слон з можливостями як у комп'ютерів, на яких мова С зі стандартною бібліотекою і з'являлася!) /ATmega328 перестав про це турбуватися.
Яещо робота більш-менш складна, то купа рядків коду зі strcpy, ctrcat, strlen, *buf++ = і так далі зрештою і важко супроводжувати, і все одно їсть багато пам'яті.
Розглядайте printf-функції як інтерпретатор DSL по форматуванню тексту :-)

І я припинив робити вручну всякі такі речі

int addBitNumberLlistTag(char *dst, int len, const char *tag_name, uint8_t bits)
{
    // Xоча краще б визначити константу десь поруч з оголошенням getBitNumberList
    char buf[2 * CHAR_BITS + 1];

    if (bits == 0)
        return snprintf(dst, len, "<%s/>",  tag_name);

    getBitNumberList(buf, bits);

    return snprintf(dst, len, "<%s>%s</%s>", tag_name, buf, tag_name);
}

Повернуте значення можна використовувати для контролю переповнення буфера

Перед початком створення даних

    pr_buf.cursor = printed_data.buf; // якось отриманий вказівник на буфер
    pr_buf.size_left = printed_data.buf_size;

Кожна порція додається якось так

    int size = addBitNumberLlistTag(pr_buf.cursor, pr_buf.size_left, "outs_on", 0x55);
    // У реальному коді наступні рядки об'єднати у функцію і замінити одним рядком
    // return buf_forward(&pr_buf, size);
    if (size >= pr_buf.size_left)
        return ERR_BUF_TOO_SMALL;

    pr_buf.size_left -= size;
    pr_buf.cursor += size;

    return OK;

Загалом прямо в addBitNumberLlistTag можна передавати вказівник на pr_buf, а він відразу може повертати buf_forward(&pr_buf, size);
У самопальному принтері даних у форматі json я приблизно так і зробив. Всі функції отрмують вказівник на json_buffer і додають своє туди.

Подякували: Betterthanyou, leofun01, MCS-513

7 Востаннє редагувалося MCS-51 (07.02.2021 19:35:38)

Re: Допоможіть написати краще

koala, ReAl, дуже вдячний за цінні поради, на жаль малий об'єм знань поки не довзволяє оцінити усю глибину викладених думок, треба більше часу, щоб осмислити (ну крім прибирання "хвостової" коми звичайно, там усе просто).

Але є ось одне запитання. Я коли читав книжку, то дуже добре запамятав, що локальні змінні всередині функції при однаковій назві із глобальними зінними, мають "пріорітет" при зверненні за цією назвою.

Тому ось щодо цього:

koala написав:

Змінювати параметри всередині функції - загалом не дуже добре...

Я і не розраховував, щоб буду всередині функції їх змінювати. Я розраховував, що буду змінювати аргументи (локальні копії цих змінних).
Спеціально зробив експеримент, створив простеньку функцію:

void func(uint8_t stateOuts)
{
  Serial.println(stateOuts);
  stateOuts++;
  Serial.println(stateOuts);
}

Потім ось так роблю її виклик:

  stateOuts = 1; // спеціально змінюємо на "не 0", щоб побачити чи є різниця між зверненням "до виклику функції", і першим зверненням усередині функції
  Serial.println(stateOuts);
  func(stateOuts);
  Serial.println(stateOuts);

У консолі бачу:
1
1
2
1

Тобто усередині функції змінювався лише аргумент (локальна копія змінної), глобальна змінна залишилася незайманою, як і планувалося. Наявність кваліфікатора volatile у змінної stateOuts не вплинула на результат.
Якби функція не мала параметру для передачі з таким самим іменем, то я звичайно змінював би глобальну змінну, як Ви і писали.
Я все вірно зрозумів, чи моя думка помилкова ?

8

Re: Допоможіть написати краще

Ви все зрозуміли абсолютно правильно. Мої міркування дещо вищого рівня: це функція не на 2 рядки. Припустимо, хтось потім спробує змінити її, щоб після списку встановлених бітів виводилося початкове число, наприклад
6 => <outs_on>2,3,6,</outs_on>
Зміна майже очевидна:

//тут цикл
itoa(stateOuts, target, 10);
while(*stateOutsStr)
    stateOutsStr++;
//тут закриваємо теґ

І... ви вже здогадалися, що станеться, правда? Буде 0, бо цикл "з'їдає" stateOuts.
Те саме - якщо ми захочемо повертати stateOutsStr в кінці функції: він уже змінений, ми повертаємо вказівник на кінець, а не на початок.
Параметри, передані до функції, бажано не змінювати, якщо це не буде одразу видно.
Ну і так, я неправий - OUTS_ON_CLOSED у загальному випадку все ж треба копіювати. Хоча іноді для оптимізації можна і параметри змінювати, і повертати стрічку без копіювання.

Подякували: MCS-511

9 Востаннє редагувалося MCS-51 (08.02.2021 22:55:26)

Re: Допоможіть написати краще

Тепер ще прошу поради у такому питанні. Є робочий шматок коду, написаний на С++ у стилі "пишу як на С, але юзаю класи, бо вони є", із використанням ардуіновського класу String.
Виглядає він ось так:

Прихований текст
const char  POST_HOST_CONTENT_LEN[] = "POST /api/device HTTP/1.1\r\nHost: " HOST "\r\nContent-Length: ";
const char  REQUEST[] =               "\r\n\r\n<request><action>";
const char  GET_DEV_DATA[] =          "get_dev_data</action></request>";
#define      REQEST_GET_DEV_DATA   String(POST_HOST_CONTENT_LEN) + \
                                                   (sizeof(REQUEST)-1 + sizeof(GET_DEV_DATA)-1 - CR_LF_CR_LF_SIZE) + \
                                                             REQUEST  +             GET_DEV_DATA

десь у основному циклі є відправка запиту:
httpClient.print(REQEST_GET_DEV_DATA);
ну і у такому стилі оформлено ще купу запитів та відповідей.

Почитав ось цю статтю: https://cpp4arduino.com/2020/02/07/how- … class.html
"Фу, кака", треба переписати на sprintf+відправка з буферу.

Ок, переписано ось так, працює:

Прихований текст
#define     REQEST_GET_DEV_DATA       sprintf(buffTransmit, "%s%s%s%s", POST_HOST_CONTENT_LEN, \
                                      itoa((sizeof(REQUEST)-1 + sizeof(GET_DEV_DATA)-1 - CR_LF_CR_LF_SIZE),itoaStr,10), \
                                                   REQUEST,            GET_DEV_DATA);

ну і ще буфери buffTransmit та itoaStr створено, ну і відправка вже йде з buffTransmit .

Очікувана реакція: виглядає не так лаконічно, але ж без зайвих класів (значить більш портабельно???), в теорії має менше місця займати.
Наявна реакція: виглядає ну дуже не лаконічно, треба думати де буфера розмістити, їх розмір (глобально, чи локально, але тоді зовсім по різному треба підходити до їх розміру, бо глобальний має забезпечувати передачу максимально можливого запиту, а якщо локальні, то їх у купі місць треба вставляти). та ще й якогось дідька їсть на 4 байти ОЗУ та FLASH більше тільки для 1 запиту, просто треш якийсь. Як би зробили ви на моєму місці ? Можна без коду, хоча б просто опишіть концепцію.

Подякували: 0xDADA11C71

10

Re: Допоможіть написати краще

MCS-51 написав:
sprintf(itoa)

Серйозно?
Нащо вам itoa, якщо sprintf використовуєте? %d. Ось вам і байти зайві повернулися.
Далі - в оригіналі воно відправляється в httpClient, тобто в результаті, я так розумію, у якийсь сокет. Сокети в C - це, зазвичай, просто файли; відповідно, fprintf відправить усе одразу куди треба, але тут дивіться, що на вашій платформі.

Подякували: MCS-511

11

Re: Допоможіть написати краще

koala написав:

Серйозно?...

Серйозно))
Займаюся програмуванням у післяробочий час, тому буває туплю на рівному місці.
Оце вже було ліг спати, але перечитав ваш пост, увімкнув пк щоб перевірити, і дійно, без ітоа -20 байт Flash, ДЯКУЮ))
Стосовно іншого, то наскільки я знаю, у ембеддед люди вміють перенаправляти вивід у UART, але я ще так не пробував, знань не вистачає.

12

Re: Допоможіть написати краще

Та глянув я апі того ардуїна - нічого цікавого там нема, усе обрізано по самі... вуха.
Хіба що не зовсім зрозуміло, нащо власний POST-запит писати (20 байтів мережі економити на юзераґенті?) і чому не можна це в кілька httpClient.print робити. Але якщо ви можете тільки цей REQEST_GET_DEV_DATA змінювати - тоді доведеться так. Ну і sprintf повертає кількість виведених байтів, а не стрічку, але я не знаю, що воно за httpClient.print(String), тому не можу нічого сказати. Документацію давайте.

Подякували: MCS-511

13 Востаннє редагувалося MCS-51 (09.02.2021 13:44:54)

Re: Допоможіть написати краще

Поки зробив отак, все працює (хіба що розмір буфера вийшов трохи із запасом, можна трохи "дотюнити", але то таке), на сьогодні я думаю вистачить))
Буфера будуть локальні, під кожен запит/відповідь - свого розміру.

Прихований текст
#define     PRINT(x)                    httpClient.print(x);\
                                        Serial.print(F("Send to HTTP host: "));\
                                        Serial.println(x);

#define     REQEST(x)                   char x[sizeof(POST_HOST_CONTENT_LEN)+sizeof(REQUEST)+sizeof(GET_DEV_DATA)] = {0};\
                                        sprintf(x, "%s%d%s%s", POST_HOST_CONTENT_LEN,\
                                        (sizeof(REQUEST)-1 + sizeof(GET_DEV_DATA)-1 - CR_LF_CR_LF_SIZE),\
                                                REQUEST,            GET_DEV_DATA);\
                                        PRINT(x);

PRINT(x)  буде використовуватись і у інших запитах/відповідях, тому виділив у окремий макрос.
Сам запит тепер виглядає отак:
REQEST(get_dev_data); // вжух, і створився буфер під запит, спрінтфом набився, відправився, у консольку дебажну вивівся і щастя наступило))

Документацію сам ще почитаю, Вас поки не турбуватиму, результат мене уже влаштовує.

Up 09.02.21.

koala написав:

...нащо власний POST-запит писати (20 байтів мережі економити на юзераґенті?) і чому не можна це в кілька httpClient.print робити...

Тому що використовуваний WiFi модуль ESP8266 не має нативної підтримки http, і мені необхідно цю обгортку над tcp формувати самому.
Тому і кілька httpClient не можна - немає ніякої буферизації на стороні модуля, один http запит має бути одним шматком.