Всех приветствую! Видимо мне выпало официально «открыть» сообщество – опубликовать первую статью ) Если, конечно, кто-то не опередит, пока я собираюсь с мыслями... Строго не судите, первый подобный опыт.
Темой статьи будет микросхема MCP3550, которую я использую во многих текущих проектах. MCP3550 – аналого-цифровой преобразователь от MicroChip, и, как заявляет сам MicroChip, его главные плюсы – низкая стоимость и малое потребление. По опыту использования – с быстродействием или с точностью, которые обеспечивает данный АЦП, проблем пока также не было.
В этой статье хочу разобрать основные особенности и характеристики микросхемы, затем формат выдаваемых данных и для закрепления результата написать небольшой демо-пример для STM32 и MCP3550.
У компании MicroChip есть широкая линейка разных аналого-цифровых устройств, среди которых можно выделить несколько больших классов:
Семейство микросхем MCP355x относится к классу сигма-дельта АЦП. Общий принцип работы сигма-дельта преобразователей в данном материале рассматривать не буду, но… если вдруг будет интерес, то обязательно расскажу, что об этом знаю. Основные характеристики MCP3550 такие:
- Разрядность – 22 бита.
- Внешний интерфейс – 3-х проводной SPI.
- Напряжение питания – от 2.7 до 5.5 В.
- Потребление – 100 мкА при напряжении питания 2.7 В, 120 мкА при напряжении питания 5.5 В.
- Температурный диапазон – от -40ºC до +125ºC. Этой характеристику часто считают не значимой, хотя в промышленных устройствах ей пренебрегать нельзя.
- 8-ми выводный корпус MSOP/SOIC. Достаточно удобно-паябельный даже в домашних условиях:
- Время выполнения аналого-цифрового преобразования:
Я буду говорить конкретно об MCP3550, потому что конкретно эта модель используется в моих проектах, но все сказанное одинаково применимо ко всей линейке MCP355x. Схема включения и подключения к внешнему микроконтроллеру очень проста:
Я опускаю стандартную обвязку микроконтроллера в виде кварцевого резонатора, конденсаторов по питанию и т. п. Также не привожу дополнительную защиту входных линий в виде диодов.
У меня используется STM32F042K6. Подключение внешнего интерфейса SPI микросхемы к STM32 происходит по трем линиям:
- CS (Chip Select)
- SCK
- SDO/RDY
Chip Select контролирует запуск преобразования. И преобразование, и отправка данных наружу происходят при низком уровне на CS. Вывод SDO/RDY сообщает о состоянии преобразователя. Высокий уровень означает, что преобразование еще выполняется, при появлении низкого уровня можно начинать обмен данными по SPI, так как MSP3550 завершил аналого-цифровое преобразование.
Возможны два режима работы: Single Conversion и Continuous Conversion:
Я буду использовать непрерывный режим, что значит – на CS всегда низкий уровень. Проверяя SDO/RDY, нужно дождаться низкого уровня, и затем можно запрашивать цифровой результат преобразования по SPI. Для этого микроконтроллер должен выдать тактирование на линию SCK, после чего можно принимать данные на входе MISO. Вывод SPI MOSI таким образом не задействован, так как микроконтроллер ничего не отправляет MCP3550. Поэтому PA7 я использую как обычный порт ввода-вывода в режиме выхода для подключения к Chip Select MCP3550.
По SPI будем принимать 3 байта (24 бита). Хотя разрядность преобразования – 22 бита, но к ним добавляются 2 служебных бита, Overflow High (OVH) и Overflow Low (OVL) для сообщения об ошибках переполнения. В итоге получаем 24 бита, ровно 3 байта – очень удобно. Из этих 24-х битов OVH является 22-м, а OVL – 23-м.
MCP3550 измеряет дифференциальное аналоговое напряжение, то есть разность напряжений между входами Vin+ и Vin-:
Vin = Vin+ - Vin-
Очень хорошо формат данных описан в даташите в виде таблицы:
Бит 21 является знаковым битом, он равен 0, если (Vin ≥ 0) и 1, если (Vin < 0).
У меня на Vref подается 3.3 В. По таблице (Vref – 1 LSB) соответствует значению 0x1FFFFF, поэтому значение Vref соответствует (0x1FFFFF + 1 = 0x200000). Тогда изменение напряжения на входе, соответствующее 1 LSB равно:
3.3 В / 0x200000 ≈ 0,000001573 В
Такова разрешающая способность микросхемы. При этом Overflow High (OVH) будет выставлен в 1 при Vin > (Vref – 1 LSB) = (3.3 - 0,00000157) В. Overflow Low (OVL) работает точно так же, только для отрицательных значений Vin, то есть случаев, когда Vin- > Vin+.
Создадим проект для MCP3550 и STM32. В STM32CubeMx задаем нужный микроконтроллер (мой случай - STM32F042K6) и настраиваем:
- PA5 - SPI1 SCK
- PA6 - SPI1 MISO
- PA7 – GPIO OUTPUT - CS
- External HSE
- SWD интерфейс отладки
- тактирование
Задаем настройки интерфейса SPI:
И тут особенно важно правильно настроить Clock Polarity (CPOL) и Clock Phase (CPHA). Почему особенно важно? Потому что при неверных настройках данные также будут приходить на контроллер, только биты будут смещены. Поэтому рассчитанное из цифрового сигнала напряжение будет неверным, но при это программа на первый взгляд будет работать без сбоев.
Генерируем код инициализации в STM32CubeMx…
Получаем готовый проект, в который добавим работу с MCP3550. Для этого примера не буду добавлять отдельные файлы, поскольку кода будет не очень много – пусть он весь будет в main.c.
Работу алгоритма построим так:
- Ожидаем низкого уровня на SDO/RDY (PA6). При этом добавим счетчик таймаута на тот случай, если по какой-то причине уровень никогда не станет низким, чтобы программа не зависла на этом ожидании навсегда.
- Принимаем 3 байта по SPI
- Проверяем биты переполнения.
- Из принятых по SPI байт рассчитываем численное значение измеренного напряжения.
Вуаля, готово, теперь в коде (main.c):
int main(void) { /* USER CODE BEGIN 1 */ /* USER CODE END 1 */ /* MCU Configuration--------------------------------------------------------*/ /* Reset of all peripherals, Initializes the Flash interface and the Systick. */ HAL_Init(); /* USER CODE BEGIN Init */ /* USER CODE END Init */ /* Configure the system clock */ SystemClock_Config(); /* USER CODE BEGIN SysInit */ /* USER CODE END SysInit */ /* Initialize all configured peripherals */ MX_GPIO_Init(); MX_SPI1_Init(); /* USER CODE BEGIN 2 */ // Кратковременно подаем высокий уровень на CS, // затем сбрасываем в 0 и так оставляем - непрерывный режим HAL_GPIO_WritePin(MCP3550_CS_PORT, MCP3550_CS_PIN, GPIO_PIN_SET); HAL_Delay(100); HAL_GPIO_WritePin(MCP3550_CS_PORT, MCP3550_CS_PIN, GPIO_PIN_RESET); /* USER CODE END 2 */ /* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ mcp3550_timeout_cnt = 0; // Ожидаем низкого уровня на SDO/RDY while((HAL_GPIO_ReadPin(MCP3550_SDO_RDY_PORT, MCP3550_SDO_RDY_PIN) == GPIO_PIN_SET) && (mcp3550_timeout_cnt < MCP3550_TIMEOUT_MS)); // Дальнейшая обработка только в том случае, если дождались низкого уровня, // и таймаут не сработал if (mcp3550_timeout_cnt < MCP3550_TIMEOUT_MS) { // Принимаем 3 (MCP3550_DATA_LEN) байта данных по SPI HAL_SPI_Receive(&hspi1, mcp3550_data, MCP3550_DATA_LEN, 100); // Из них формируем uint32_t переменную для удобства обработки uint32_t result = (mcp3550_data[0] << 16) | (mcp3550_data[1] << 8) | (mcp3550_data[2]); if ((result & MCP3550_OVF_BIT_MASK) != 0) { // Произошло событие переполнения } else { // Переполнения нет, проверяем знак if ((result & MCP3550_SIGN_BIT_MASK) == 0) { // Если неотрицательный результат - записываем сразу в mcp3550_lsb mcp3550_lsb = result; } else { // Если отрицательный результат преобразуем результат, об этом // я напишу в тексте статьи ниже mcp3550_lsb = (~result); mcp3550_lsb &= MCP3550_DATA_BIT_MASK; mcp3550_lsb += 1; mcp3550_lsb *= (-1); } // Получаем напряжение в вольтах mcp3550_voltage = mcp3550_lsb * MCP3550_LSB_V; // Цель достигнута } } } /* USER CODE END 3 */ }
В прерывании по SysTick таймеру увеличиваем счетчик (stm32f0xx_it.c):
void SysTick_Handler(void) { /* USER CODE BEGIN SysTick_IRQn 0 */ /* USER CODE END SysTick_IRQn 0 */ HAL_IncTick(); /* USER CODE BEGIN SysTick_IRQn 1 */ mcp3550_timeout_cnt++; /* USER CODE END SysTick_IRQn 1 */ }
Для использования mcp3550_timeout_cnt
в этом файле добавляем в начале файла stm32f0xx_it.c в секции «External variables»:
/* USER CODE BEGIN EV */ extern uint32_t mcp3550_timeout_cnt; /* USER CODE END EV */
Пояснение процесса расчета вольт из принятых данных... Макрос MCP3550_LSB_V
определяет, скольким вольтам соответствует изменение данных на 1 наименее значимый бит:
#define MCP3550_LSB_V MCP3550_VREF_V / MCP3550_DATA_MAX
Теперь переводим принятые по SPI данные в кол-во LSB, как в этой чудо-таблице:
Задача – в переменной mcp3550_lsb
получить значение, соответствующее столбцу Decimal Code. После чего это значение умножается на MCP3550_LSB_V
, и получаются вольты. При положительных значениях Vin (красная область таблицы) просто приравниваем mcp3550_lsb
и result
. При отрицательных значениях (синяя область) – инвертируем, убираем старшие биты, прибавляем 1 и умножаем на (-1).
Допустим пришло значение 0x3FFFFE (result = 0x3FFFFE
):
- Инвертировали, получили – 0xС00001.
- Старшие биты – не биты данных, а флаги переполнения, которые мы уже проверили. Поэтому их просто обнулим (
mcp3550_lsb &= MCP3550_DATA_BIT_MASK
). Получили 0x000001, что есть 1. - Прибавили 1, получили 2.
- Умножили на (-1), получили -2 (-2 LSB, как в таблице).
- Дальше рассчитываем вольты и все.
Я постарался снабдить код текстом, но, если возникнут вопросы, могу пояснить в комментариях, обращайтесь. Либо через мой профиль на сайте. Ссылка на проект - mcp3550_project.
На это все, спасибо за внимание! Вот результат работы программы:
Было бы интересно почитать доступным языком про сигма-дельта преобразование.
Спасибо за интерес!
Остаётся объединить усилия и сделать его на классах.
Добрый день. А как поймать синхронизацию данных, если случайно сбились в SPI без использования CS?