orangepig

Energymon R1.01

По просьбам радиослушателей в новую версию:

* добавлено удобное подключение внешнего источника питания
* увеличена точность измерений на микроамперном диапазоне (INA214 → OPA2376 + LM7705) и заодно полоса частот (30 кГц → 50 кГц)
* падение напряжения на верхнем диапазоне измерения (3 А) уменьшено до 50 мВ
* добавлено измерение фактического напряжения питания
* добавлено отображение на встроенном экране минимального, максимального и текущего токов потребления
* добавлен расчёт интегрального потребления в мВт*ч

Всё это по-прежнему в габаритах спичечного коробка (37,5 × 50 мм).



На этом приём пожеланий по аппаратной части закончен, платы ушли в печать.

Ну и ценник с 1 февраля поднимется с 2490 до 2990 в связи со всем этим.

P.S. Отвечая на вопрос из мессенджера: все, кто уже оплатил предзаказы или планирует это сделать до 1 февраля, получат сразу новую ревизию. Первая ревизия никому не рассылалась и рассылаться не будет, это был десяток образцов для отладки, проверки, оценки возможностей и показа фоток почтенной публике.

orangepig

Измерение энергопотребления IoT-устройств в реальных условиях

Пока бложик на olegart.ru лежит (не совсем навсегда, но в целом я думаю, что с ним дальше сделать — и, скорее всего, все старые записи грохну, начну по-новой и несколько в ином формате), рассказываю тут, какую мы клёвую штуку сделали, пока все бухали.



Это измеритель реального энергопотребления IoT-устройств, а также примкнувшим к ним UMDK-ENERGYMON (по ссылке длинное описание):

  • диапазон измеряемых токов от 1 мкА до 3 А (да, я знаю, что это больше 6 порядков) при максимальном падении напряжения на самом измерителе 150 мВ, а типовом менее 50 мВ

  • три автоматически переключаемых шунта, make-before-break, скорость реакции на скачки тока — от 5 мкс

  • измерения каждые 10 мкс, аналоговая полоса пропускания 30 кГц

  • напряжение питания нагрузки, регулируемое от 2,0 до 3,5 В с шагом 0,1 В

  • возможность запитать нагрузку от внешнего блока до 24 В

  • выдача в компьютер по USB результатов измерения в мкА, усреднённых по отрезкам 100 мс

  • отображение на экранчике прошедшего времени и накопленных мА*ч

  • встроенный мост USB-UART, встроенный отладчик SWD с интерфейсом DAPLink, ввод STM32 и аналогичных в режим загрузчика одним нажатием кнопки

Аналогов, насколько мне известно, не то чтобы нет совсем, но за эти деньги — точно.

Предзаказывать тут. Оплата безналом, доставка СДЭКом, стоимость доставки можно посмотреть в калькуляторе на сайте СДЭК (по Москве — 300 рублей).

До нового года мы сделали маленькую партию на опыты, сейчас посмотрим на объём заказов и отправим на изготовление (1-2 недели, не больше) новую ревизию.
orangepig

Программный переход в бутлоадер на произвольном STM32

В продолжение темы про способ определения валидности произвольного адреса в памяти на Cortex-M — пример практического использования.

В конкретном изделии иногда хочется программно прыгнуть на штатный фабричный бутлоадер STM32 — чтобы перейти в режим обновления прошивки по UART или USB, не утруждая себя написанием своего бутлоадера.

Бутлоадер у STM32 лежит в области под названием System Memory, на которую и надо перейти, но есть одна проблема — у этой области разные адреса не то что на разных сериях процессоров, а на разных моделях одной серии (с эпической табличкой можно ознакомиться в AN2606 на страницах с 22 по 26). При внесении же соответствующего функционала в платформу вообще, а не просто в конкретное изделие, хочется универсальности.

В файлах CMSIS адрес начала System Memory также отсутствует. Определить его по Bootloader ID не представляется возможным, т.к. это проблема курицы и яйца — Bootloader ID лежит в последнем байте System Memory, что возвращает нас к вопросу об адресе.

Однако, если мы посмотрим на карту памяти STM32, то увидим примерно такую картину:

Нас в данном случае интересует окружение System Memory — например, сверху лежит однократно программируемая область (есть не во всех STM32) и Option bytes (есть во всех). Эта структура наблюдается не то что в разных моделях, а в разных линейках STM32, с разницей лишь в наличии OTP и наличии зазора в адресах между системной памятью и опциями.

Но для нас в данном случае важнее всего то, что адрес начала Option Bytes есть в штатных заголовках CMSIS — он там называется OB_BASE.

Дальнейшее просто. Пишем функцию поиска первого валидного или невалидного адреса вниз или вверх от указанного:

char *cpu_find_next_valid_address(char *start, char *stop, bool valid) {
    char *address = start;
    while (true) {       
        if (address == stop) {
            return NULL;
        }
        
        if (cpu_check_address(address) == valid) {
            return address;
        }
        
        if (stop > start) {
            address++;
        } else {
            address--;
        }
    };

    return NULL;
}

И ищем вниз от Option bytes сначала конец то ли системной памяти, то ли примыкающих к ней OTP, а потом и начало системной памяти — в два захода:

/* System memory is the valid area next _below_ Option bytes */
char *a, *b, *c;
a = (char *)(OB_BASE - 1);
b = 0;

/* Here we have System memory top address */
c = cpu_find_next_valid_address(a, b, true);

/* Here we have System memory bottom address */
c = cpu_find_next_valid_address(c, b, false) + 1;

И без особого труда оформляем это в функцию, находящую начало системной памяти и прыгающую на него, то есть — запускающую бутлоадер:

static void jump_to_bootloader(void) __attribute__ ((noreturn));

/* Sets up and jumps to the bootloader */
static void jump_to_bootloader(void) {
    /* System memory is the valid area next _below_ Option bytes */
    char *a, *b, *c;
    a = (char *)(OB_BASE - 1);
    b = 0;
    
    /* Here we have System memory top address */
    c = cpu_find_next_valid_address(a, b, true);
    
    /* Here we have System memory bottom address */
    c = cpu_find_next_valid_address(c, b, false) + 1;
    
    if (!c) {
        NVIC_SystemReset();
    }
    
    uint32_t boot_addr = (uint32_t)c;
    
    uint32_t boot_stack_ptr = *(uint32_t*)(boot_addr);
    uint32_t dfu_reset_addr = *(uint32_t*)(boot_addr+4);

    void (*dfu_bootloader)(void) = (void (*))(dfu_reset_addr);

    /* Reset the stack pointer */
    __set_MSP(boot_stack_ptr);

    dfu_bootloader();
    while (1);
}

От конкретной модели процессора тут зависит… да ничего не зависит. Логика не будет работать на моделях, у которых между OTP и системной памятью есть дырка — но я не проверял, есть ли такие вообще. Будете активно работать с OTP — проверьте.

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

P.S. Так как все адреса в карте памяти процессора выровнены в худшем случае по 4, описанную выше процедуру кратно ускорит идея шагать по ним с шагом 4 байта вместо одного.

Автоматической кросс-пост из уютного бложика olegart.ru

orangepig

Написал всякого

Всё основано на реальных событиях. Из уважения к погибшим, имена выживших оставлены без изменений.

Завтра мы начнём вас убивать, или Зачем нужны инженеры

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

Основы электробезопасности при проектировании электронных устройств

Тема это большая и сложная, но я постараюсь выделить основные моменты — не в последнюю очередь на основании ошибок, которые я видел во всевозможных реальных устройствах и проектах, в том числе публиковавшихся на Хабре. Я не буду долго и нудно перечислять ГОСТы, но перечислю совсем базовые вещи, которые необходимо понимать и соблюдать, чтобы не убить хотя бы себя (если вы планируете не убивать также и окружающих, то после завершения этой статьи не поленитесь пролистать и релевантные ГОСТы).

Тем временем в комментах на Хабре сообщают, что продаваемые под маркой Чип-и-Дипа модули — это с точки хрения электробезопасности полный пиздец.

Посмотрел, подтверждаю. Проектировавший это жопорукий кретин не имеет понятия о существовании в мире напряжений больше, чем на пальчиковой батарейке. Использовать эту продукцию для коммутации напряжений выше 50 В опасно для жизни и имущества.

Автоматической кросс-пост из уютного бложика olegart.ru

orangepig

Зима идёт, зиме дорогу

Вот вам, дети, лайфхак (в новостях прошу писать «российский предприниматель разработал уникальную») про погодные станции.

Во многих из них внешние датчики работают от двух батареек АА, которые и в принципе-то не очень долго служат (их ёмкость в районе 2 Вт*ч), а на морозе так и вовсе теряют две трети ёмкости. Менять их в наших краях предлагается на две литиевые батарейки по 1,5 В, имеющие 4,5-5,5 Вт*ч номинальной ёмкости, работающие от -40 °С и стоящие в розницу ~400 рублей пара.

Соединены они при этом тупо последовательно, т.е. выдают 3,0 В.

Что позволяет совершенно спокойно поменять одну из них на перемычку, а вторую — на LiSOCl2 батарейку ER14505 ёмкостью 8 Вт*ч, работающую от -60 °С и стоящую 250-300 рублей в розницу (ну а у тех, кто занимается IoT, их и так как цветочков за баней, по полтора бакса штука).

Тем, у кого бани нет — вот: https://www.chipdip.ru/catalog-show/cylindrical-batteries?x.1513=exT&x.265=UUT&x.2094=Hxs&sort=priceup. Но, с другой стороны, если бани у вас нет, то и разница в 100 рублей с парой литиевых полуторавольтовых вам вряд ли принципиальна.

Не благодарите.

P.S. Для датчиков с питанием от AAA лайфхак не работает. Ставьте Energizer Ultimate Lithium полуторавольтовые.

P.P.S. Если сама погодная станция тоже работает от двух батарек, то с ней тоже так можно — морозостойкость ей не нужна, но срок службы батарейки поднимется вдвое. Вообще со всем, что работает от двух AA в последовательном включении, так можно.

P.P.P.S. Рассчитанная на 3,0 В электроника 3,6 В переносит примерно всегда — absolute max voltage у неё как раз обычно и есть в районе 3,6-3,8 В (но если вы очень беспокоитесь, есть CR14505 на 3,0 В и 5,4 Вт*ч). Есть вариант с хитровыебнутой схемотехникой, в которой средняя точка батарей тоже зачем-то нужна, но он крайне маловероятный.

Автоматической кросс-пост из уютного бложика olegart.ru

orangepig

Чип видишь? А он есть!

Про серию материалов Блумберга о китайских закладках в материнках Supermicro я скажу коротко — следующим актом драмы я ожидаю исторический полёт главреда из окна санатория с криком «узкоглазые идут!».

Во-первых, предецент есть, во-вторых, ничем другим это объяснить уже нельзя.

https://t.me/sforsecurity/60

Автоматической кросс-пост из уютного бложика olegart.ru

orangepig

Проверка корректности адресов в памяти на Cortex-M0/M3/M4/M7

Одна из весьма полезных и при этом почему-то в готовом виде нигде не описанных возможностей на микроконтроллерах Cortex-M (всех) — это возможность проверки корректности адреса в памяти. С её помощью можно определять размеры флэша, ОЗУ и EEPROM, определять наличие на конкретном процессоре конкретной периферии и регистров, прибивать упавшие процессы при сохранении общей работоспособности ОС и т.п.

В штатном режиме при попадании в несуществующий адрес на Cortex-M3/M4/M7 вызывается исключение BusFault, а при отсутствии его обработчика эскалируется до HardFault. На Cortex-M0 «детализированных» исключений (MemFault, BusFault, UsageFault) нет, а любые сбои сразу эскалируются до HardFault.

В общем случае игнорировать HardFault нельзя — он может быть следствием аппаратного сбоя, например, и дальнейшее поведение устройства станет непредсказуемым. Но в частном случае это делать можно и нужно.

На Cortex-M3 и выше проверка валидности адреса делается достаточно просто — надо запретить все исключения (кроме, очевидно, немаскируемых) через регистр FAULTMASK, отключить конкретно обработку BusFault, а потом ткнуть в проверяемый адрес и посмотреть, не взвёлся ли флажок BFARVALID в регистре BFAR, то есть Bus Fault Address Register. Если взвёлся — у вас только что был BusFault, т.е. адрес некорректный.

Код выглядит так, все дефайны и функции из стандартного (не вендорского) CMSIS, так что должен работать на любом M3, M4 или M7:

bool cpu_check_address(volatile const char *address)
{
    /* Cortex-M3, Cortex-M4, Cortex-M4F, Cortex-M7 are supported */
    static const uint32_t BFARVALID_MASK = (0x80 << SCB_CFSR_BUSFAULTSR_Pos);
    bool is_valid = true;

    /* Clear BFARVALID flag */
    SCB->CFSR |= BFARVALID_MASK;

    /* Ignore BusFault by enabling BFHFNMIGN and disabling interrupts */
    uint32_t mask = __get_FAULTMASK();
    __disable_fault_irq();
    SCB->CCR |= SCB_CCR_BFHFNMIGN_Msk;

    /* probe address in question */
    *address;

    /* Check BFARVALID flag */
    if ((SCB->CFSR & BFARVALID_MASK) != 0)
    {
        /* Bus Fault occured reading the address */
        is_valid = false;
    }

    /* Reenable BusFault by clearing  BFHFNMIGN */
    SCB->CCR &= ~SCB_CCR_BFHFNMIGN_Msk;
    __set_FAULTMASK(mask);

    return is_valid;
}

С Cortex-M0 и Cortex-M0+ всё сложнее, как я сказал выше, у них нет BusFault и всех соответствующих регистров, а исключения сразу эскалируются до HardFault. Поэтому выход один — сделать так, чтобы обработчик HardFault смог понять, что исключение было вызвано преднамеренно, и вернуться обратно в вызвавшую его функцию, передав туда некий флажок, указывающий, что HardFault был.

Делается это сугубо на ассемблере. В примере ниже регистр R5 устанавливается в 1, а в регистры R1 и R2 записываются два «магических числа». Если после попытки загрузить значение по проверяемому адресу случится HardFault, то он должен проверить значения R1 и R2, и при обнаружении в них нужных чисел установить R5 в ноль. В сишный код значение R5 передаётся через специальную переменную, жёстко привязанную к этому регистру, в ассемблер проверяемый адрес — в неявной форме, мы просто знаем, что в arm-none-eabi первый параметр функции кладётся в R0.

bool cpu_check_address(volatile const char *address)
{
    /* Cortex-M0 doesn't have BusFault so we need to catch HardFault */
    (void)address;
    
    /* R5 will be set to 0 by HardFault handler */
    /* to indicate HardFault has occured */
    register uint32_t result __asm("r5");

    __asm__ volatile (
        "ldr  r5, =1            \n" /* set default R5 value */
        "ldr  r1, =0xDEADF00D   \n" /* set magic number     */
        "ldr  r2, =0xCAFEBABE   \n" /* 2nd magic to be sure */
        "ldrb r3, [r0]          \n" /* probe address        */
    );

    return result;
}

Код обработчика HardFault в простейшем виде выглядит так:

__attribute__((naked)) void hard_fault_default(void)
{
    /* Get stack pointer where exception stack frame lies */
    __asm__ volatile
    (
        /* decide if we need MSP or PSP stack */
        "movs r0, #4                        \n" /* r0 = 0x4                   */
        "mov r2, lr                         \n" /* r2 = lr                    */
        "tst r2, r0                         \n" /* if(lr & 0x4)               */
        "bne use_psp                        \n" /* {                          */
        "mrs r0, msp                        \n" /*   r0 = msp                 */
        "b out                              \n" /* }                          */
        " use_psp:                          \n" /* else {                     */
        "mrs r0, psp                        \n" /*   r0 = psp                 */
        " out:                              \n" /* }                          */

        /* catch intended HardFaults on Cortex-M0 to probe memory addresses */
        "ldr     r1, [r0, #0x04]            \n" /* read R1 from the stack        */
        "ldr     r2, =0xDEADF00D            \n" /* magic number to be found      */
        "cmp     r1, r2                     \n" /* compare with the magic number */
        "bne     regular_handler            \n" /* no magic -> handle as usual   */
        "ldr     r1, [r0, #0x08]            \n" /* read R2 from the stack        */
        "ldr     r2, =0xCAFEBABE            \n" /* 2nd magic number to be found  */
        "cmp     r1, r2                     \n" /* compare with 2nd magic number */
        "bne     regular_handler            \n" /* no magic -> handle as usual   */
        "ldr     r1, [r0, #0x18]            \n" /* read PC from the stack        */
        "add     r1, r1, #2                 \n" /* move to the next instruction  */
        "str     r1, [r0, #0x18]            \n" /* modify PC in the stack        */
        "ldr     r5, =0                     \n" /* set R5 to indicate HardFault  */
        "bx      lr                         \n" /* exit the exception handler    */
        " regular_handler:                  \n"

        /* here comes the rest of the fucking owl */
    )

В момент ухода в обработчик исключения Cortex скидывает регистры, которые гарантированно будут испорчены обработчиком (R0-R3, R12, LR, PC…), в стек. Первый фрагмент — он уже есть в большинстве готовых обработчиков HardFault, кроме написанных под чистый bare metal — определяет, в какой именно стек: при работе в ОС это может быть либо MSP, либо PSP, и у них разные адреса. В bare metal проектах обычно априори устанавливается стек MSP (Main Stack Pointer), без проверки — ибо PSP (Process Stack Pointer) там быть не может в силу отсутствия процессов.

Определив нужный стек и положив его адрес в R0, мы читаем из него значения R1 (смещение 0x04) и R2 (смещение 0x08), сравниваем с магическими словами, если оба совпадают — читаем из стека значение PC (смещение 0x18), добавляем к нему 2 (2 байта — размер инструкции на Cortex-M) и сохраняем обратно в стек. Если этого не сделать, при возвращении из обработчика мы окажемся на той же инструкции, которая собственно и вызвала исключение, и будем вечно бегать по кругу. Добавление 2 перемещает нас на следующую инструкцию в момент возвращения.

Далее пишем 0 в R5, что служит индикатором попадания в HardFault. Регистры после R3 до специальных регистров в стек не сохраняются и при выходе из обработчика никак не восстанавливаются, поэтому портить или не портить их — на нашей совести. В данном случае R5 с 1 на 0 мы меняем целенаправленно.

Возвращение из обработчика прерывания делается строго одним способом. При входе в обработчик в регистр LR пишется специальное значение под названием EXC_RETURN, которое для выхода из обработчика надо записать в PC — и не просто записать, а сделать это командной POP или BX (то есть «mov pc, lr», например, не работает, хотя по первому разу вам может показаться, что работает). BX LR выглядит как попытка перехода по бессмысленному адресу (в LR будет лежать что-то вида 0xFFFFFFF1, не имеющее никакого отношения к реальном адресу процедуры, в которую нам надо вернуться), но в реальности процессор, увидев это значение в PC (куда оно ляжет автоматически), сам восстановит регистры из стека и продолжит выполнять нашу процедуру — со следующей после вызвавшей HardFault процедуры благодаря тому, что PC в этом стеке мы руками увеличили на 2.

Прочитать про все смещения и команды можно понятно где, разумеется.

Ну или если магических чисел не видно, то всё перейдёт к regular_handler, после которого идёт обычная процедура обработки HardFault — как правило, это сишная функция, печатающая в консоль значения регистров, решающая, что дальше делать с процессором и т.п.

Использование всего этого просто и понятно. Хотим написать прошивку, которая работает на нескольких микроконтроллерах с разным объёмом ОЗУ, при этом каждый раз используя ОЗУ по полной программе?

Да легко:

static uint32_t cpu_find_memory_size(char *base, uint32_t block, uint32_t maxsize) {
    char *address = base;
    do {
        address += block;
        if (!cpu_check_address(address)) {
            break;
        }
    } while ((uint32_t)(address - base) < maxsize);

    return (uint32_t)(address - base);
}

uint32_t get_cpu_ram_size(void) {
    return cpu_find_memory_size((char *)SRAM_BASE, 4096, 80*1024);
}

maxsize здесь нужен затем, что на максимально возможном объёме ОЗУ между ним и следующим блоком адресов может не быть зазора, на котором сломается cpu_check_address. В данном примере это 80 КБ. Все адреса прощупывать также нет смысла - достаточно по даташиту посмотреть, каков минимально возможный шаг между двумя моделями контроллера, и поставить его в качестве block.

Не благодарите.

Автоматической кросс-пост из уютного бложика olegart.ru

orangepig

Китайские напряжометры

У любого мультиметра с функцией измерения напряжения есть, по большому счёту, две задачи:

  • Показать вам величину, более-менее соответствующую напряжению в цепи, в которую вы воткнули его щупы
  • Не уебать вас током насмерть

В последнее время на почве появления контроллера DTM0660, который обладает программной калибровкой и позволяет любому китайцу сделать мультиметр, удовлетворяющий первому требованию, интернеты наполнились радостными отзывами о всевозможных Аненгах, Холдпиках и прочих продуктах с алиэкспресса, поражающих соотношением цены к качеству.

Хотелось бы, однако, в связи с этим прояснить второе требование.

Начнём с теории. Требования к мультиметру по способности не уебать вас током обычно представлены у него на корпусе надписью в духе «600V CAT III» (если этой надписи нет, то всё плохо), которую обычно понимают в духе «можно измерять до 600 В».

Это не совсем так. Первая часть надписи — «600V» — означает не только способность не сдохнуть при подаче 600 В, но, главное, способность встроенных цепей защиты эффективно сработать в случае, если мультиметр, будучи под напряжением 600 В, сдохнет по причине, напрямую с этим напряжением не связанной (например, вы сунете щупы в трёхфазную сеть в режиме амперметра).

Например, если в мультиметре стоит стеклянный полый предохранитель, рассчитанный на 250 В, то при таком эксперименте он вместо размыкания цепи способен устроить зажигание дуги внутри себя, с дальнейшим взрывом его корпуса и иными неприятными последствиями.

Вторая часть — CAT III — описывает, в каких цепях можно проводить такие эксперименты без риска для своей жизни:

  • CAT I — использование в безопасных цепях питания, гальванически развязанных от электросети
  • CAT II — использование в бытовых внутриквартирных электросетях
  • CAT III — использование в сетях электропитания здания и внутренних электрощитках
  • CAT IV — использование в сетях на вводе в здание

Причина такого деления проста: сети отличаются не только напряжением, но и наличием собственной защиты. Внутриквартиная сеть при коротком замыкании в общем случае может отоваривать вас 16 амперами, прежде чем сработает её пакетник; элетрощит на лестничной площадке — сотнями ампер, а элетрощит на вводе в здание — тысячами. Это само по себе повышает вероятность зажигания дуги вместо обрыва в неподходящем предохранителе, ну а что сделает такой ток с кусочком пластика у вас в руках — можете сами представить.

Кроме того, чтобы не доводить до греха, каждой категории и её номинальному напряжению соответствует также величина импульсов, которые устройство обязано выдерживать без пробоя. Например, 600V CAT II должен выдерживать до 4 кВ, CAT III — 6 кВ, CAT IV — 8 кВ. Максимальная категория — 1000V CAT IV — выдерживает 12 кВ и почти не встречается в природе.

А теперь берём отвёртку и смотрим внутрь нескольких мультиметров (фоточки не мои).

Collapse )

Автоматической кросс-пост из уютного бложика olegart.ru