stm8l adc определение напряжения питания мк

Контроллеры STM8L15х имеют на борту 12-и разрядный АЦП, который может работать в нескольких режимах, и поддерживает работу с контроллером DMA, что позволяет оцифровать и сложить в память кучу данных без участия ядра.

Здесь я попытался собрать как можно больше информации об АЦП в STM8, чтобы не пришлось бегать по другим статьям в поисках кода для настройки таймера, или, например, DMA. Вот, что описано в статье:
— Настройка АЦП
— Выполнение преобразований в разных режимах
— Настройка внешнего триггера для запуска преобразования
— Настройка таймера для работы совместно с АЦП
— Использование встроенного датчика температуры
— Настройка контроллера DMA для работы вместе с АЦП
— Использование Analog Watchdog

Семейство STM8S не рассматриваем — там все сильно по-другому. А в STM8L101 АЦП вообще нет.

Для начала, ТТХ преобразователя:
— Программируемое разрешение: 6, 8, 10 или 12бит (снижение разрешения позволяет увеличить скорость работы)
— До 28 внешних каналов. Но в том МК, что стоит на STM8L-Discovery, их «всего» 25.
— Плюс еще два внутренних канала: термодатчик и опорное напряжение.
— Два основных режима работы:
1) Одиночное преобразование
2) Последовательное чтение нескольких каналов, с переброской данных в буфер силами DMA. При этом МК не отвлекается на переключение каналов или запись данных в буфер.
— Внешние и внутренние триггеры.
— В качестве опорного напряжения выступает либо пин Vdda, либо ножка Vref+ (в таком случае напряжение на ней должно быть > 2.4V). В дискавери VREF наглухо прибита к питанию.
 

Настройка

Перед началом работы неплохо-бы разобраться, как его включить. Как и с другой периферией, на АЦП надо подать тактирование. За это отвечает самый первый бит в регистре CLK_PCKENR2:

CLK->PCKENR2 |= CLK_PCKENR2_ADC1; //Подаем тактирование на АЦП

Изначально АЦП находится в спящем режиме. Чтобы его разбудить, надо поднять бит ADON в регистре ADC_CR1. Но сразу-же начинать преобразование нельзя — нужно подождать примерно 3мкс, пока преобразователь проснется.

Если АЦП будет простаивать некоторое время без дела, то он сам уйдет в спящий режим. Это «некоторое время» — Tidle — обычно равно 1 сек, но может резко уменьшиться с ростом температуры.

После того, как АЦП проснулся, надо выбрать каналы, с которых мы хотим оцифровать сигнал. Можно выбрать сразу несколько каналов через регистры ADC_SQR1 — ADC_SQR4. Биты СHSEL_Sx в них отвечают за каждый канал. Для выбора канала просто устанавливаем соответствующий бит (если установить несколько, то они будут обрабатываться начиная с младшего). Биты CHSEL_STS и CHSEL_SVREFINT отвечают за выбор термодатчика и опорного напряжения соответственно.
 

В заголовочном файле stm8l15x.h регистры ADC_SQRx организованы в массив, поэтому нумерация начинается с 0. Например, вот так

ADC1->SQR[3] |= (1<<4);

мы установим 4й бит в регистре ADC_SQR4.

К примеру, если выставить биты вот так, то будет обработана последовательность каналов: 0, 2, 6, 8, 10, 14, 18.

В регистрах TRIGR1TRIGR4 (которые тоже увязаны в массив, поэтому нумерация с нуля) каждый бит отвечает за отключение триггера Шмитта на соответствующем канале. При записи в бит 1, триггер отключается и пин нельзя использовать как цифровой вход. Но если их не отключать — точность будет на порядок хуже.

К примеру, при замере напряжения с потенциометра при включенном триггере результаты могли отличаться на несколько десятков LSB. При отключении триггера это различие уменьшалось до пары LSB.

Обрати внимание, что в регистре ADC_SQR1 скрывается бит DMAOFF, который отвечает за работу АЦП совместно с DMA. По умолчанию он сброшен, а значит, DMA контроллер включен. Если DMA тебе не нужен, то в этот бит надо записать 1.

Биты SMP1[2:0] в регистре ADC_CR2 управляют периодом выборки АЦП (sampling time) для первых 24 каналов. Для остальных (в дискавери это последний внешний канал и два внутренних) есть биты SMP2[2:0] в ADC_CR3.

Биты SMPx[2:0] могут принимать такие значения:
000 — Время выборки = 4 такта АЦП
001 — 9 тактов
010 — 16 тактов
011 — 24 тактов
100 — 48 тактов
101 — 96 тактов
110 — 192 такта
111 — 384 такта

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

Подробнее про ошибку, возникающую из-за внешнего сопротивления, можно прочитать в аппноуте под названием "A/D converter on STM8L devices:
description and precision improvement techniques
" в разделе «External resistance design error»

Если нет необходимости использовать все 12 бит, то разрешение АЦП можно снизить. Для этого существуют биты RES[1:0] в регистре ADC_CR1:
00 — 12 бит
01 — 10 бит
10 — 8 бит
11 — 6 бит

От разрешения и времени выборки зависит скорость АЦП. Время необходимое на одно преобразование считается по такой формуле:

Tconv = (ts+n)/fADC

Из этого легко получить максимальную скорость работы АЦП:

ADCSpeed = fADC/(ts+n)

В этих формулах
fADC — Частота тактового сигнала на АЦП
ts — время выборки (в тактах)
n — разрешение АЦП

Например, при частоте тактирования в 8МГц, разрешении в 10 бит и минимальном времени выборки (4 такта) мы можем получить скорость 571.4 кГц. Если по каким-то причинам надо уменьшить скорость, то можно установить бит PRESC в регистре ADC_CR2. От этого преобразователь начнет работать в два раза медленнее.

Для примера вот простейшая инициализация АЦП:

CLK->PCKENR2 |= CLK_PCKENR2_ADC1; //Тактирование

ADC1->CR1 |= ADC_CR1_ADON; //Пинаем АЦП, чтобы он проснулся

ADC1->SQR[0] |= ADC_SQR1_DMAOFF; //Отключаем DMA
ADC1->SQR[3] |= 1<<2; //Выбираем 2 канал (пин A4 на STM8L152C6T6)

После выполнения этого кода АЦП будет настроен на разрешение 12бит и минимальное время выборки — это параметры по-умолчанию.

Настраивать АЦП мы научились, теперь хочется что-то измерить и преобразовать.
 

Режим одиночного преобразования

Самый простой режим, в котором АЦП после запуска считывает напряжение с одного канала и останавливается. Если в регистрах ADC_SQRx выбрано больше одного канала, то будет обработан тот, у кого номер больше.

Перед преобразованием надо (кроме обычной настройки АЦП, см. выше) отключить DMA. Для этого устанавливаем бит DMAOFF в регистре ADC_SQR1.

Чтобы начать преобразование, устанавливаем бит START в регистре ADC_CR1. Как только оно завершится — поднимется флаг EOC в ADC_SR. Именно по нему надо определять завершение преобразования, ибо бит START сбрасывается сразу-же после начала. Бит EOC сбрасывается сам после чтения результата преобразования (если точнее, то после чтения регистра ADC_DRL).

Если установлен бит EOCIE в регистре ADC_CR1, то произойдет прерывание по завершению преобразования. При использовании прерывания, в его обработчике надо обязательно читать регистр ADC_DRL, иначе флаг прерывания не сбросится и, после выхода из обработчика, контроллер опять вернется к нему.

Результат хранится в регистрах ADC_DRH и ADC_DRL с выравниванием по правому краю.

Читать следует сначала старший регистр, а потом — младший. Хотя если установлено разрешение в 6 или 8 бит, то старший читать не обязательно.

Вот так можно запустить преобразование и прочитать результат в переменную result (uint16):
(перед этим АЦП должен быть правильно настроен (см. выше))
 

ADC1->CR1 |= ADC_CR1_START; //Поехали!

while ((ADC1->SR && ADC_SR_EOC) == 0); //Ждем, пока выполнится преобразование

//Забираем результат
result = ADC1->DRH << 8;
result |= ADC1->DRL;

В архиве находится пример для IAR, который замеряет напряжение на входе A4.
 

Про работу с примерами:
Обычно в подобных примерах результат измерений выводится в UART или на индикатор.
Но я решил поступить по другому — использовать для этих целей отладчик, встроенный в STM8L-Discovery.
В примере результат просто считывается в переменную result, значение которой можно наблюдать через отладчик:

Кому-то может показаться, что это не удобно, но зато программа освобождается от лишнего кода.

Режим непрерывной работы

Установка бита CONT в регистре ADC_CR1 запускает АЦП в режиме непрерывного преобразования. Он считывает значения всех выбраных каналов, начиная с младшего. Как только дойдет до последнего канала — может сгенерить прерывание EOC и начать с начала.

В этом режиме необходимо использовать контроллер DMA, потому-что прерывание EOC происходит только, когда завершено преобразование всех выбраных каналов и считать результат из регистров ADC_DRH и ADC_DRL нельзя.

Если АЦП стартует от внешнего триггера, то после запуска в режиме непрерывного преобразования он будет выполнять преобразования до тех пор, пока не будет остановлен. Сделать это можно двумя способами: сбросить бит CONT в регистре ADC_CR1 — АЦП остановится, когда завершит преобразование текущую цепочку каналов; или сбросить бит ADON в том-же ADC_CR1, тем-самым незамедлительно выключив преобразователь.

Так-же можно разрешить работу через DMA (и правильно его настроить, о чем дальше) без установки бита CONT. В таком случае АЦП преобразует все выбраные каналы, DMA запишет результат в оперативку, после чего АЦП остановится.
 

Запуск преобразования от внешних триггеров

АЦП может запускаться не только через флаг START, но и от внешних (по отношению к преобразователю) триггеров. В STM8L15x их три штуки:
— От пинов A6 или D0 (TRIG1)
— По событию от таймера 1 (TRIG2)
— По событию от таймера 2 (TRIG3)

Для выбора триггера предназначены биты EXTSEL1 и EXTSEL0 в ADC_CR2. Они могут принимать следующие значения:

00 — Программный запуск
01 — TRIG1 (пин A6 или D0)
10 — TRIG2 (Таймер 1)
11 — TRIG3 (Таймер 2)

Фронт сигнала от триггера, по которому запустится АЦП, выбирается битами TRIG_EDGE1 и TRIG_EDGE0 в том-же ADC_CR2:

00 — Reserved
01 — Передний фронт
10 — Задний фронт
11 — Оба фронта

Для 1 триггера это означает передний (возрастающий) или задний (убывающий) фронт на выбраном пине. Для остальных двух — фронт сигнала соответствующего события (задается при настройке таймера). Для них логичнее всего выбирать передний фронт — тогда АЦП запустится сразу, как появилось событие.
 

TRIG1 — Сигнал с пина

Первый триггер может переключаться между пинами A6 и D0. По умолчанию он настроен на A6. Для переключения на D0 нужно установить бит ADC1TRIG_REMAP, который находится в регистре SYSCFG_RMPCR2.

Никакой дополнительной настройки ножки, с которой будет подаваться стартовый сигнал на АЦП, не требуется. Вот такой код настроит АЦП на запуск от падения напряжения на пине A6:

GPIOA->CR1 |= GPIO_Pin_6; //Подтягивающий резистор на пин A6 (не обязательно). у меня там висела кнопка.

CLK->PCKENR2 |= CLK_PCKENR2_ADC1; //Тактирование

ADC1->CR1 |= ADC_CR1_ADON; //Будим АЦП
ADC1->CR2 |= (1<<6) | (1<<3); //Старт по заднему фронту от 1 триггера.

ADC1->SQR[0] |= ADC_SQR1_DMAOFF; //Отключаем DMA
ADC1->SQR[3] |= 1<<2; //Выбираем 2 канал (пин A4 на STM8L152C6T6)

Теперь, как только напряжение на пине A6 упадет до лог 0, будет запущено преобразование.
 

TRIG2, TRIG3 — Таймеры

Первый или второй таймер могут использоваться в качестве источника стартового сигнала для АЦП. У каждого из них есть выход TRGO (Trigger output), который внутри кристалла соединяется со входом триггера АЦП. Таймеры могут дергать этот выход по разным поводам — переполнение счетчика, совпадение счетчика с регистром сравнения, и т.д. АЦП принимает сигнал и начинает преобразование.

Мы рассмотрим самый простой режим, в котором таймер считает от 0 до какого-либо значения, при достижении которого дает сигнал АЦП и снова обнуляется. Таким образом АЦП будет выполнять преобразования через нужные нам промежутки времени.

Настройка таймера для наших целей выглядит вот так:

  CLK->PCKENR1 |= CLK_PCKENR1_TIM2; //Подаем тактирование на таймер.

  //Волшебное число 46875 получилось так:
  // Частота МК (2М) / Предделитель таймера (128) * Нужное время в секундах (3)
  TIM2->ARRH = 46875>>8; //Записываем сначала старший байт
  TIM2->ARRL = (uint8_t)(46875); //Потом младший
  TIM2->CR1 |= TIM_CR1_URS; //Настраиваем источник события update
  TIM2->CR2 |= (2<<4); //Настраиваем на подачу сигнала для АЦП при переполнении таймера
  TIM2->PSCR = 7; //Предделитель = 2^7 = 128
  TIM2->EGR |= TIM_EGR_UG;  //Генерим событие update, чтобы обновился предделитель
  TIM2->CR1 |= TIM_CR1_CEN; //Запускаем

Думаю пояснения тут излишни. Все самое важное указано в комментах к коду.

Если все-же что-то непонятно, то есть примерчик в котором таймер подает сигнал на АЦП каждые 500мс. В прерывании по завершению преобразоывания переключается синий светодиод — чтобы было видно как часто срабатывает АЦП.
 

Использование встроенного термодатчика

В STM8L есть свой датчик температуры, подключенный к каналу ADC_TS в АЦП. Особо высокой точностью он, конечно, не отличается, но измерения типа «тепло/холодно» выполнять может.

Перед тем, как читать показания датчика, его надо запустить (изначально выключен в целях экономии электричества). Для управления питанием датчика служит бит TSON в регистре ADC_TRIG1. После того, как он будет установлен, надо подождать 10 микросекунд, чтобы датчик успел включиться.

Время выборки (для термодатчика оно устанавливается битами SMTP2[2:0]) советуют установить больше 10 микросекунд. Если поставить меньше, то высокое выходное сопротивление датчика будет вносить искажения в результат. А может и не будет, но рекомендации соблюдать надо.

Кроме запуска, работа с датчиком ничем не отличается от работы с любым другим каналом.

Код, запускающий термодатчик, и считывающий его показания в переменную result:
 

CLK->PCKENR2 |= CLK_PCKENR2_ADC1; //Тактирование

ADC1->CR1 |= ADC_CR1_ADON;
ADC1->TRIGR[0] |= ADC_TRIGR1_TSON; //Подаем питание на датчик

ADC1->CR2 |= 3; //Sampling time = 12uS
ADC1->SQR[0] |= ADC_SQR1_DMAOFF | (1<<5); //Отключаем DMA и выбираем датчик температуры.

delay_10us(1); //Функция из delay.h


ADC1->CR1 |= ADC_CR1_START; //Начинаем измерение

while ((ADC1->SR && ADC_SR_EOC) == 0); //Ждем, пока выполнится преобразование

//Забираем результат
result = ADC1->DRH << 8;
result |= ADC1->DRL;

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

Измерение опорного напряжения

В STM8L есть встроенный ИОН, что не удивительно. Удивительно то, что использовать его в качестве опорного напряжения для АЦП нельзя. Но напряжение ИОНа можно измерить при помощи АЦП. Нужно это, к примеру для того, чтобы расчитать напряжение питания МК. Или чтобы сделать поправку в измеряемых значениях (при нестабильной внешней опоре).

Опорное напряжение в STM8L15x около 1.224V (минимальное — 1.202, максимальное — 1.242). При тестировании МК на заводе оно было измерено и сохранено в память по адресу, который называется VREFINT_Factory_CONV. Значение адреса нагугливается в даташите на МК, в разделе «Memory and register map» -> «Register map» -> «Factory conversion registers». Например для МК на STM8L-Discovery этот адрес 0x4910. В заголовочных файлах этот адрес не упоминается, поэтому если мы хотим прочитать значение опорного напряжения, то нужно прописать адрес. Примерно так:

#define Factory_VREFINT 0x4910

И далее, читаем как обычный регистр:

uint16_t ref;
... 
ref = 0x600 + Factory_VREFINT;

Старший байт этого значения всегда равен 0x06, а по адресу VREFINT_Factory_CONV находится младший.

Для того, чтобы опорное напряжение можно было замерить через АЦП, нужно установить бит VREFINTON в регистре ADC_TRIGR1.

После этого можно измерять напряжение через канал VREFINT (Пятый бит в ADC_SQR1).

Используя значение опорного напряжения можно скорректировать результат преобразования:

В этой формуле
Din — измеренное напряжение
Vrefint — значение опорного напряжения в вольтах. Взятое из даташита или измеренное ранее.
Drefint — результат замера опорного напряжения.

Заменив в этой формуле Din на максимальное значение для данного разрешения (1023 для 10 бит, 4095 для 12 бит ...) мы получим в результате значение опорного напряжения АЦП. А если оно подключено к пину Avcc, то таким образом можно довольно точно определить напряжение питания.

Это может быть полезно в случае устройства, которое питается от батарей безо всяких преобразователей. Или, если устройство может питаться от нескольких источников с разным напряжением — можно определять от какого источника девайс питается в данный момент.
 

Использование DMA

Часто возникает необходимость собрать кучу выборок с АЦП в оперативку. Для этого можно использовать контроллер DMA. В отличие от ручной переброски данных в RAM, тут от нас требуется только настроить DMA правильным образом и запустить АЦП.

Вся работа DMA сводится к тому, что при завершении преобразования он забирает данные из регистров ADC_DRH и ADC_DRL, записывает их в буфер по указаному адресу, и инкрементирует этот адрес. При этом контроллер DMA отсчитывает количество байт, которые уже записаны. Как только записалось нужное количество — происходит прерывание.

Нам понадобится буфер, куда DMA будет складывать данные:
 

uint16_t recv_array[4];

Названия битов и регистров DMA в заголовочном файле STM8L15x.h соответствуют даташиту чуть менее, чем никак. Непонятно для чего STшники так сделали, но догадываться о назначении того или иного бита приходится только по описанию.

Теперь настраиваем DMA:
 

CLK->PCKENR2 |= CLK_PCKENR2_DMA1; //Подаем тактирование

  DMA1_Channel0->CNBTR = sizeof(recv_array)-1; //Размер буфера
  
  DMA1_Channel0->CPARH = (ADC1_BASE+4)>>8; //Адрес регистра АЦП (старший байт)
  DMA1_Channel0->CPARL = (uint8_t)(ADC1_BASE+4); //Младший
  
  DMA1_Channel0->CM0ARH = (uint8_t)((uint16_t)recv_array>>8); //Адрес буфера
  DMA1_Channel0->CM0ARL = (uint8_t)recv_array;  
  
  DMA1_Channel0->CSPR |= DMA_CSPR_16BM;  //Режим работы с 16и битными числами.
  DMA1_Channel0->CCR |= DMA_CCR_IDM | DMA_CCR_CE | DMA_CCR_TCIE; //Включаем канал и разрешаем прерывание
  DMA1->GCSR |= DMA_GCSR_GE; //Включаем DMA

После запуска АЦП, DMA будет складывать результаты преобразования в массив recv_array. Когда он заполнится — сработает прерывание Transaction Complete. Если прерывание не нужно, то бит DMA_CCR_TCIE должен быть сброшен.

Если разрядность АЦП 8 или 6 бит (то-есть результат помещается в регистр ADC_DRL), то строчку:
 

DMA1_Channel0->CSPR |= DMA_CSPR_16BM;  //Режим работы с 16и битными числами.

Из процедуры настройки можно убрать. А при указании адреса регистра,
 

DMA1_Channel0->CPARH = (ADC1_BASE+4)>>8; //Адрес регистра АЦП (старший байт)
  DMA1_Channel0->CPARL = (uint8_t)(ADC1_BASE+4); //Младший

цифру 4 заменить на 5. Тогда DMA будет считывать 1 байт из регистра ADC_DRL (который имеет смещение 5 относительно базового адреса АЦП) и записывать его в массив.

Еще можно установить бит CIRC в регистре CCR. В этом случае при заполнении буфера DMA не остановится, а начнет заполнять его с начала, перезаписывая старые данные
 

DMA1_Channel0->CCR |= DMA_CCR_ARM; //ARM - Auto-Reload mode

При этом можно отключить прерывания и наслаждаться тем, что в буфере постоянно находятся свежие данные с каналов (ядро в их обновлении не участвует).

Надо отметить, что если включен режим непрерывного преобразования (бит CONT), но при этом не установлен бит CIRC, то при заполнении буфера DMA остановится, но АЦП продолжит работать. Данные при этом никуда сохраняться не будут.

В этом примере при помощи DMA данные с 4 каналов (1,2,3,4) складываются в массив в оперативной памяти.

Каналы соответствуют следующим пинам:
1 — A5
2 — A4
3 — C7
4 — C4
 

Analog Watchdog

АЦП имеет одну полезную фичу — мониторинг напряжения на каком-либо канале.
Работает она так:
После завершения преобразования сравнивается номер текущего канала с тем, который нужно отслеживать. Если они совпали, то результат преобразования сравнивается с двумя порогами — нижним и верхним. Если результат больше верхнего порога или меньше нижнего, то поднимается флаг AWD в регистре ADC_SR. Можно установить бит AWDIE в ADC_CR1, тогда analog watchdog сможет вызывать прерывание.

Флаг AWD нужно опускать вручную, записав в него 0. Это создает проблему: момент, когда напряжение выходит за установленные рамки мы отловить можем, а вот вернулось-ли оно обратно в эти рамки придется проверять вручную.

Канал, который будет отслеживать analog watchdog выбирается через биты CHSEL[4:0] в регистре ADC_CR3. Значение 0 соответствует нулевому каналу, 1 — первому, и т.д. Через регистры ADC_HTRH и ADC_HTRL настраивается верхний порог, а через ADC_LTRH и ADC_LTRL — нижний.

Значения порогов записываются с учетом текущего разрешения АЦП. Например 1/4 опорного напряжения это 64 для разрешения 8 бит, или 1024 для разрешения 12 бит.

Настройку порогов и выбор канала нужно проводить до запуска АЦП.

Вот таким образом можно настроить Analog Watchdog (предполагается, что АЦП уже настроен):
 

ADC1->CR1 |= ADC_CR1_AWDIE; //Разрешаем прерывание AWD

//Устанавливаем верхний (4000) и нижний (100) пороги
#define AWD_low_th 100
#define AWD_high_th 4000

ADC1->HTRH = AWD_high_th>>8;
ADC1->HTRL = (uint8_t)(AWD_high_th);

ADC1->LTRH = AWD_low_th>>8;
ADC1->LTRL = (uint8_t)(AWD_low_th);

ADC1->CR3 &= ~ADC_CR3_CHSEL; //Сбрасываем канал
ADC1->CR3 |= 2; // и выбираем канал №2 для отслеживания

Теперь можно либо мониторить бит AWD в ADC_SR (если прерывание не используется),

if ((ADC1->SR & ADC_SR_AWD) != 0) 
 {
  . . .
 }

либо ждать прерывание. В его обработчике нужно будет сбросить бит AWD, записав в него 0:
 

INTERRUPT_HANDLER(ADC_IRQ, 18)
{
 if ((ADC1->SR & ADC_SR_AWD) != 0) 
 {
   . . .
   ADC1->SR &= ~ADC_SR_AWD;  
 }
}

На этом всё. Работа с АЦП во всех возможных режимах описана, примеры для быстрого старта и «для поиграццо» есть. Остался один вопрос — в следующий раз описывать сабж так-же подробно, или достаточно короткого описания, как в статье про lcd?

Добавить комментарий

Обратная связь

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

пишите мне на netdm@mail.ru