1. Введение
На повестке дня стояла задача разработать протокол общения микроконтролера nrf52832 с двумя полумостовыми китайскими тензодатчиками.
Задача оказалась не простой, так как столкнулся с отсутствием какой — либо внятной информации. Вероятнее, что «корень зла» находится в самом SDK от Nordic Semiconductor — это постоянное обновления версий, некоторая избыточность и запутанность функционала. Пришлось писать все с нуля.
Я думаю эта тема довольно актуальна исходя из того, что данный чип обладает BLE стеком и целым набором “вкусняшек” режима энергосбережения. Но в техническую часть я сильно углубляться не буду, так как на эту тему написано немало статей.
2. Описание проекта
Железо:
- 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
Выводы
Спасибо всем за внимание, надеюсь эта статья будет полезной и сократит драгоценное время на поиск решения для разработчиков. Хочу сказать, что технический подход который использует Nordic в своих платформах довольно интересен с точки зрения энергоэффективности.
P.S.
Проект еще в процессе разработки, поэтому если будет интересна эта тема в следующей статье я постараюсь описать алгоритм калибровки датчиков веса, а также подключения BLE стека.
IronHead
Я конечно не работал с HX711, но мне кажется ее интерфейс проще подключить к SPI.
Тогда вся статья свелась бы к
и последующим вызовом
lamerok
У него на той же линии готовность еще идет. Если завести ее на отдельную ногу и там считывать ее состояние, и еще надо считывать данные по заднему фронту по идее… По документации на АЦП вроде как при переходе с 1 в 0 попадаем на данные, но автор почему то переходу из 0 в 1 считывает.
osmanpasha
В настройках SPI микроконтроллеров же обычно можно указать, по какому фронту читать-посылать данные?
А в NRF можно повесить прерывание на ногу MISO? Если да, то вроде и дополнительную ногу можно не использовать.
lamerok
Обычно в микроконтроллерах есть конечно, наверное в этом тоже, я не знаю, я с ним не работал… Просто в примера выше, такой настройки не производится вроде, не знаю что означает
spi_config.mode = NRF_DRV_SPI_MODE_1;
, может это как раз?Я так понимаю, он по прерыванию будет работать, но по SPI прерыванию — уходу/приходу байта, или просто в режиме опроса флага, пока байт не уйдет/придет. Но даже если он по опросу будет делать, нога в этот момент используется как spi MISO и не может одновременно использоваться как прерывание от порта и как аппаратный MISO spi.
mctMaks
Не совсем так. На самом деле можно так делать. Только лучше использовать не прерывание, а событие.
У нордиков очень своеобразная периферия, по меркам ST даже бедновато. НО, при правильном подходе к ней она дает огромное преимущество.
Например, можно сделать более красиво и изящно, периферия это позволяет. Опишу немного абстрактно как это можно сделать:
0. Заводиться группа PPI. В нее добавляем один канал, который будет по-сути DREADY сигналом. Группа нужно из-за одной замечательной фукнции, ее можно включать\выключать через PPI.
1. Канал PPI настраиваем на старт чтения через SPI и отключение группы. Это нужно, чтобы не реагировать на каждый перепад фронта во время чтения;
2. второй канал PPI нужен для обратного включения группы после окончания чтения SPI.
дополнительно можно ещё и на счетчик это дело бросить, тогда ядрышко проснется только после получения, скажем 10, отсчетов.
если сильно интересно, могу поделиться своим опытом с nrf52840. Я на нем как раз с++ осваиваю)
vitalichik Автор
Вы правельно подметили, там действительно опечатка.
nrf_drv_gpiote_in_config_t gpiote_config = GPIOTE_CONFIG_IN_SENSE_HITOLO(true). Спасибо!!!
vitalichik Автор
Я думаю что вы правы, я увлекся китайским даташитом )). Будет время проверю.