Как подключить АЦП HX711 к NRF52832

1. Введение


На повестке дня стояла задача разработать протокол общения микроконтролера nrf52832 с двумя полумостовыми китайскими тензодатчиками.

Задача оказалась не простой, так как столкнулся с отсутствием какой — либо внятной информации. Вероятнее, что «корень зла» находится в самом SDK от Nordic Semiconductor — это постоянное обновления версий, некоторая избыточность и запутанность функционала. Пришлось писать все с нуля.


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


2. Описание проекта


image

Железо:


  • Adafruit Feather nRF52 Bluefruit LE (то что оказалось под рукой)
  • АЦП HX711
  • Китайские тензодатчики 2 шт. (50х2 кг)
  • Программатор ST-LINK V2

Софт:


  • IDE VSCODE
  • NRF SDK 16
  • OpenOCD
  • Программатор ST-LINK V2


Все находится в одном проекте, придется только подшаманить Makefile (указать расположение вашего SDK).


3. Описание кода


Будем использовать GPIOTE модуль для работы с периферией исходя из привязки задач и событий, а также PPI модуль для передачи данных из одной периферии в другую без участия процессора.



ret_code_t err_code;
   err_code = nrf_drv_gpiote_out_init(PD_SCK, &config);//настраеваем на выход
   nrf_drv_gpiote_out_config_t config = GPIOTE_CONFIG_OUT_TASK_TOGGLE(false);//будем передергивать пин для импульса
   err_code = nrf_drv_gpiote_out_init(PD_SCK, &config);//настраеваем на выход

Настраиваем линию синхронизации PD_SCL на выход для генерации импульсов длительностью 10 мкс.

   nrf_drv_gpiote_in_config_t  gpiote_config = GPIOTE_CONFIG_IN_SENSE_HITOLO(false);// переход уровня с высокого на низкий
   nrf_gpio_cfg_input(DOUT, NRF_GPIO_PIN_NOPULL);// на вход без подтяжки
   err_code = nrf_drv_gpiote_in_init(DOUT, &gpiote_config, gpiote_evt_handler); 

static void gpiote_evt_handler(nrf_drv_gpiote_pin_t pin, nrf_gpiote_polarity_t action)
{
    nrf_drv_gpiote_in_event_disable(DOUT);//отключаем прерывание
    nrf_drv_timer_enable(&m_timer0);//включаем таймер
}
 

Настраиваем линию данных DOUT для считывания состояния готовности HX711, при наличии низкого уровня срабатывает обработчик в котором отключаем прерывание и запускаем таймер для генерации синхронизирующих импульсов на выходе PD_SCL .


 err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channel1);
   APP_ERROR_CHECK(err_code);
   err_code = nrf_drv_ppi_channel_assign(m_ppi_channel1,                                         nrf_drv_timer_event_address_get(&m_timer0, NRF_TIMER_EVENT_COMPARE0),                                           nrf_drv_gpiote_out_task_addr_get(PD_SCK));// подключаем таймер к выходу
   APP_ERROR_CHECK(err_code);
   err_code = nrf_drv_ppi_channel_enable(m_ppi_channel1);// включаем канал
   APP_ERROR_CHECK(err_code);
   nrf_drv_gpiote_out_task_enable(PD_SCK); 
// включаем gpiote

После чего инициализируем PPI модуль и коммутируем наш таймер к выходу PD_SCL, для генерирования импульсов длительность 10мкс при наступление события сравнения, а также включаем GPIOTE модуль.



nrf_drv_timer_config_t timer_cfg = NRF_DRV_TIMER_DEFAULT_CONFIG;// по умолчанию
   timer_cfg.frequency = NRF_TIMER_FREQ_1MHz;// тактируем на частоте 1Мгц
   ret_code_t err_code = nrf_drv_timer_init(&m_timer0, &timer_cfg, timer0_event_handler);
   APP_ERROR_CHECK(err_code);
   nrf_drv_timer_extended_compare(&m_timer0,
                                  NRF_TIMER_CC_CHANNEL0,
                                  nrf_drv_timer_us_to_ticks(&m_timer0,
                                                            10),
                                  NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK,
                                  true);// срабатывает по сравнению

Инициализируем нулевой таймер и его обработчик.



  if(m_counter%2 != 0 && m_counter<=48){
       buffer <<= 1;// переменная считанных даных
        c_counter++;// счетчик положительных  импульсов
           if(nrf_gpio_pin_read(DOUT))buffer++;//считываем состояние входа
   }

Самое интересное происходит в обработчике таймера. Период импульсов составляет 20 мкс. Нас интересуют нечетные импульсы (по восходящему фронту) и при условии, что их количество не более 24, а событий — 48. При каждом нечетном событии происходит считывание DOUT


Из даташита следует, что количество импульсов должно быть не менее 25, что соответствует коэффициенту усиления 128 (в коде я использовал 25 импульсов), это эквивалентно 50 событиям таймера, что указывает на окончание фрейма данных.


 ++m_counter;// счетчик событий
if(m_counter==50){
      nrf_drv_timer_disable(&m_timer0);// отключаем таймер
       m_simple_timer_state = SIMPLE_TIMER_STATE_STOPPED;//
       buffer = buffer ^ 0x800000;
       hx711_stop();//jотключаем hx711
       }
   

После этого отключаем таймер и обрабатываем данные (по даташиту) и переводим HX711 в режим низкого энергопотребления.



static void repeated_timer_handler(void * p_context)
{
   nrf_drv_gpiote_out_toggle(LED_2);
   if(m_simple_timer_state == SIMPLE_TIMER_STATE_STOPPED){
      	hx711_start();// включаем hx711
       nrf_drv_gpiote_out_toggle(LED_1);
       m_simple_timer_state = SIMPLE_TIMER_STATE_STARTED;
   }
  
}
/**@brief Create timers.
*/
static void create_timers()
{
   ret_code_t err_code;
 
   // Create timers
   err_code = app_timer_create(&m_repeated_timer_id,
                               APP_TIMER_MODE_REPEATED,
                               repeated_timer_handler);
   APP_ERROR_CHECK(err_code);
}

Ожидаем события от RTC таймера с интервалом в 10 с (эту уже на ваше усмотрение) в обработчике запускаем HX711, вызывая прерывание по линии DOUT.

Есть еще один момент, логи выводятся через UART (baud rate 115200, TX — 6 пин, RX — 8 пин) все настройки находятся в sdk_config.h

image

Выводы


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


P.S.


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


Материалы


Комментарии 15

    +5

    Я конечно не работал с HX711, но мне кажется ее интерфейс проще подключить к SPI.
    Тогда вся статья свелась бы к


    nrf_drv_spi_config_t spi_config = NRF_DRV_SPI_DEFAULT_CONFIG(SPI_INSTANCE);
    spi_config.frequency = SPI_FREQUENCY_FREQUENCY_K500;
    spi_config.mode = NRF_DRV_SPI_MODE_1;
    spi_config.miso_pin = SPI_MISO_PIN;
    spi_config.mosi_pin = SPI_MOSI_PIN;
    spi_config.sck_pin = SPI_CLK_PIN;
    APP_ERROR_CHECK(nrf_drv_spi_init(&spi, &spi_config, 0));

    и последующим вызовом


    nrf_drv_spi_transfer(&spi, tx_data, 1, rx_data_, 1);
      0

      Там на той же линии (DOUT) готовность еще идет. Если завести ее на отдельную ногу и там считывать ее состояние и запускать передачу, когда она упадет, то по идее можно. И еще, вроде как надо считывать данные по заднему фронту… В документации на АЦП вроде как при переходе с 1 в 0 попадаем на данные, но автор почему то считывает данные по переходу из 0 в 1.

        0

        В настройках SPI микроконтроллеров же обычно можно указать, по какому фронту читать-посылать данные?
        А в NRF можно повесить прерывание на ногу MISO? Если да, то вроде и дополнительную ногу можно не использовать.

          0
          В настройках SPI микроконтроллеров же обычно можно указать, по какому фронту читать-посылать данные?

          Обычно в микроконтроллерах есть конечно, наверное в этом тоже, я не знаю, я с ним не работал… Просто в примера выше, такой настройки не производится вроде, не знаю что означает spi_config.mode = NRF_DRV_SPI_MODE_1;, может это как раз?


          А в NRF можно повесить прерывание на ногу MISO?

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

            0
            Но даже если он по опросу будет делать, нога в этот момент используется как spi MISO и не может одновременно использоваться как прерывание от порта и как аппаратный MISO spi.


            Не совсем так. На самом деле можно так делать. Только лучше использовать не прерывание, а событие.
            У нордиков очень своеобразная периферия, по меркам ST даже бедновато. НО, при правильном подходе к ней она дает огромное преимущество.

            Например, можно сделать более красиво и изящно, периферия это позволяет. Опишу немного абстрактно как это можно сделать:

            0. Заводиться группа PPI. В нее добавляем один канал, который будет по-сути DREADY сигналом. Группа нужно из-за одной замечательной фукнции, ее можно включать\выключать через PPI.

            1. Канал PPI настраиваем на старт чтения через SPI и отключение группы. Это нужно, чтобы не реагировать на каждый перепад фронта во время чтения;

            2. второй канал PPI нужен для обратного включения группы после окончания чтения SPI.

            дополнительно можно ещё и на счетчик это дело бросить, тогда ядрышко проснется только после получения, скажем 10, отсчетов.

            если сильно интересно, могу поделиться своим опытом с nrf52840. Я на нем как раз с++ осваиваю)
          0
          Вы правельно подметили, там действительно опечатка.
          nrf_drv_gpiote_in_config_t gpiote_config = GPIOTE_CONFIG_IN_SENSE_HITOLO(true). Спасибо!!!
          0
          Я думаю что вы правы, я увлекся китайским даташитом )). Будет время проверю.
          0
          nRF Connect for Desktop не пробывали? В комплекте идёт Toolchain Manager, через который устанавливается nRF Connect SDK (на данный момент актуальная версия 1.3.1)
            0
            оно по другую тему. nRF Connect SDK — эта штука заточена больше по работу с операционкой зефир. Отсюда тенятся необходимость доустановки средств сборки зефира и проектов на нем. Не сложно, но долго.
            Да и задача тут такая, что в обычном NRF5_SDK она решается проще.
            0
            У нордика самый ублюдочный SDK с высоченным пирогом абстракций, в котором слои легаси идут вперемешку с обновлённым кодом. И документация такая же.
              –1
              Не соглашусь насчет документации, она более приятная чем у тех же ST или TI.

              Это вы еще не пробовали вести свои проекты в этом SDK… у них на форуме про SDK мелькает фраза иногда «welcome to SDK pain!»
                0
                Это вы еще не пробовали вести свои проекты в этом SDK…

                Я как раз не только пробовал, а три проекта сдал на этом куске дерьма.
                0
                Видимо, из-за этого они решили перейти на Zephyr.
                0
                Длительность в 10мкс на импульс это примерно в 10 раз больше значения из даташита,, а минимальное и вовсе 0.2мкс. Учитывая что надо сделать 25 импульсов, с периодом 20 мкс, процесс считывания можно радикально сократить, что может быть полезно в энергосберегающем режиме при работе от батареек.
                  0
                  Да вы абсолютно правы по даташиту типичная длительность импульса 1мкс, макс. 50мкс. Это был тестовый вариант проекта только, чтоб подтвердить его работоспособность. Спасибо.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое