Программирование STM8 (обучающий мастер класс)

Промышленное устройство на STM8 с Modbus с нуля. Весь код и инструменты — внутри.

В этом курсе вы не просто напишете "мигающий светодиод". Вы с нуля разработаете прошивку для полевого устройства, которое:

  • Работает в промышленной сети Modbus RTU через RS-485.

  • Построено на базе доступного STM8S003F3P6.

  • Написано в профессиональной среде IAR Embedded Workbench.

Я раскрою архитектурные секреты и практические приемы, которые обеспечивают стабильную работу "в поле". Все инструменты для старта — бесплатны. Готовы заглянуть под капот коммерческого продукта?

Для STM8 существует библиотека абстракции Standard Peripheral Library, которая ускоряет создание кода, но в этом цикле уроков я буду разрабатывать код без этой библиотеки, дабы хорошо изучить работу STM8 на низком уровне, вникнуть в процессы и попробовать посвятить в них и вас, читатель. Это, прежде всего, большая исследовательская деятельность. Надеюсь, она станет увлекательной.

Так как я не использую какую-то универсальную отладочную плату, то в качестве обучающей платы могу лишь порекомендовать взять наше изделие MERSON 24A2 (страница в интернет магазине) https://ozon.ru/t/DyJPevR и (страница с подробным техническим описанием) https://dev.ipasoft.info/doku.php?id=m24a2 . И, как я говорил выше, я веду разработку нашего собственного устройства, и прямо по ходу пьесы пишу эти статьи (мне не трудно, а вам пригодится), значит я затрагиваю только ту периферию, которая мне интересна в рамках той электрической схемы, с которой работаю. Я прямо здесь предоставляю вам принципиальную схему устройства MERSON 24A2 https://cloud.as.life/s/3BYQTYALn6DezXQ 
Если у вас достаточно опыта в разработке, то вы сможете самостоятельно адаптировать эти уроки под свое устройство или китайские "пули"/"пилюли".

Подготовка среды

Программирование этого микроконтроллера начинается с установки среды разработки IAR для STM8. Если вы не смогли найти откуда скачать установочный файл, берите с архива https://cloud.as.life/s/K3e4t5sjKirxHg3 это версия 3.114 от 2021 года. Я именно ей пользовался. 
На этом сайте я ранее писал статьи https://ipasoft.info/index.php/articles/lesson-1 по программированию STM8 на STVD от самой STMicroelectronics. Но, как известно, своего Си компилятора у этой студии нет, а сторонний компилятор Cosmic хоть и бесплатный, но сильно зависит от ежегодной активации. Его нельзя активировать раз и навсегда, увы. Поэтому я рекомендую IAR. Он платный, но есть  пробная версия с ограничением по объему кода (8 кБ), чего вполне хватит для работы с «мелкими» камнями наподобие STM8S003F3P6. 
После установки среды IAR создаем новый пустой Сишный проект: File → Project → Create new project. В этом проекте одна лишь главная функция main() с моментальным завершением return. Данная заготовка позволит нам быстро собрать первую прошивку. Меню Project → Make (или клавиша F7) и проект скомпилируется. Толку от него мало, но зато мы убедились, что среда и компилятор рабочие.
Обратите внимание на то, что при редактировании main.c кириллица будет вводиться символами вопросов.

Вы можете настроить кириллицу в IDE v3.x. Правда там определенный ритуал будет.
1) Идите в Tools → Options → Editor. Выберите кодировку UTF-8.
2) Закройте открытые файлы (в частности main.c) и откройте заново. Кириллица должна уже вводиться.

Разработка STM8 с нуля – это всегда медленный старт. Шаг за шагом вам необходимо осваивать каждую периферию, знакомясь с особенностями взаимодействия с ней. И чтобы все было последовательно, я не стану конфигурировать сразу всю периферию, а буду постепенно, модуль за модулем настраивать и рассказывать. А делается это обязательно с технической документацией на руках. Это прежде всего, даташит https://cloud.as.life/s/CoNMDzxLj8qgo7o и референс мануал https://cloud.as.life/s/Np7pysDjbycY9cc  
Важно! Перед тем как вы начнете отлаживать программу на реальном железе, проверьте настройки отладчика, у вас должен быть указан ваш программатор. У меня ST-Link. А по умолчанию может стоять вообще симулятор.

Еще убедитесь в том, что выбрано правильное устройство stm8s003 на вкладке General Options → Target.

А еще, наверняка вам захочется дополнительно генерировать файл прошивки в формате Intel HEX. Для этого нужно прогуляться на вкладку Output converter

Теперь я внесу еще один штрих в main.c – а именно подключу библиотеку описания периферии моего микроконтроллера iostm8s003f3.h, подключу вспомогательную библиотеку описания целочисленных типов stdint.h. Все они идут в комплекте с IAR компилятором.
После правки main.c нужно откомпилировать код клавишей F7 дабы убедиться, что все работает.

В консоли build выводится довольно обобщенная информация, я бы порекомендовал настроить это окошко на вывод всех деталей:

На этом, полагаю можно пока остановиться. Рабочее место подготовлено.

Hello, world

На плате MERSON 24A2, линия PC5 может «помигать светодиодом». По многолетней традиции все студенты здороваются с миром и кайфуют от результатов своей работы, потому что это очень высокая ступенька, на которую не так то просто взобраться. 
Без лишних разъяснений предлагаю набрать, скомпилировать (F7) следующий код (см листинг ниже).
Затем нужно подключить к компьютеру ваш программатор ST-Link, а к программатору подсоединить плату MERSON 24A2 (разъем P4, соответствие выводом смотрите на схеме), затем залить прошивку (Ctrl + D). Нажмите F5 (Resume) или Ctrl + Shift + D (Halt) и увидите заветные мигания желтого светодиода.
Полагаю, у вас все получилось. И если это так, то вы теперь умеете прошивать свою железку.

GPIO

GPIO – General ports inputs & outputs (общие порты ввода/вывода) работают довольно просто. Вы можете прочитать, есть или нет напряжения на определенных ножках (пинах) микросхемы в режиме «вход», или выставить (подать/снять) напряжение на ножках в режиме «выход». Отсутствие напряжения на ножке это логический ноль. Присутствие напряжения — это логическая единица. Если пин работает на вход, то любое напряжение от 0.7*VDD до VDD воспринимается как логическая единица. Если пин работает на выход, то выходное напряжение логической единицы будет приблизительно равным напряжению питания (VDD). На устройстве MERSON 24A2 используются вот такие линии GPIO:
1) PC4 TRIAC — выход (анод оптопары твердотельного реле),
2) PC5 I_DET — вход (если есть ток нагрузки, то будет лог. 1), 
3) PC6 SHN1 – выход (шунт канала 1), 
4) PC7 SHN2 – выход (шунт канала 2), 
5) PD4 U_DE – выход (пин управления драйвером RS-485). 
Я, как и любой другой нормальный человек, плохо запоминаю где какой пин находится. Да и вообще предпочитаю не хранить в голове или на листочке эту информацию, ведь можно создать псевдонимы периферии. Это в программистском жаргоне называется «задефайнить пользовательскую периферию».
В main.c дефайним битовые доступы

Теперь внутри main() в main.c начинаем конфигурировать GPIO. Для этого смотрим в RM (Reference Manual).

Для конфигурации GPIO нам доступно три регистра: 
1) DDR – data direct. Он определяет, линия порта вход (0) или выход (1).
2) CR1 – control 1. Для входа он определяет наличие (1) и отсутствие (0) подтягивающих резисторов. Для выхода определяет наличие (1) отсутствие (0) sink транзистора. Если Sink транзистор есть, то выход будет Push-Pull, а если нет, то Open Drain. Запомните: не все пины можно подтягивать к Vdd (pull-up), например этих pull-up резисторов нет на линиях SDA/SCL (PB5/PB4). Запомните, линии порта помеченные как True Open Drain (T), те же самые PB5/PB4 не могут работать в режиме Push-Pull.
3) CR2 – control 2. Для входа он включает (1) или выключает (0) прерывание линии порта. Для выхода он включает (1) или выключает (0) режим высокой скорости.

Что будет на выходе, какой уровень, 1 или 0? За уровень на каждом выходе отвечает регистр ODR (Output data register). В устройстве, мне нужно чтобы при подаче питания на всех выходах был уровень 0. Смотрим в RM и видим

Значение каждого бита по-умолчанию (после сброса) уже стоит 0.
По-умолчанию, все линии портов ввода-вывода (GPIO) являются входами, без подтяжки, не генерируют прерываний. Таким образом, линия детектора тока SEN (I_DET) (PC5) уже по-умолчанию настроена правильно, и какого-то дополнительного кода писать не требуется.
Код требуется написать лишь для конфигурирования линий вывода. 

Вам наверняка покажется странным и неправильным то, что я по одному биту конфигурирую каждый выход, ведь мог бы пачкой сделать PC_DDR = xxxx, что трачу процессорное время и делаю много лишних телодвижений. Все именно так. Однако я демонстрирую стиль, в котором сочетаются удобство прочтения кода, удобство перехода на другой процессор и удобство быстрой смены распиновки устройства (при необходимости). 
И в завершение этого урока факультативно сделаем высоковольтный Hello-world. К устройству MERSON 24A2 можно подключать 220-вольтовую нагрузку невысокой мощности. Вполне подойдет лампа накаливания на 95 Ватт. На обойму силовых клемм подключите шнур питания 220В к выводам L и N, затем подключите лампу накаливания к выводам N и AUTO. Соблюдайте меры предосторожности при работе с высоким напряжением! Не касайтесь платы под напряжением. Все подключения и отключения проводов проводите убедившись, что они обесточены.

Допишите в main(), в бесконечный цикл, этот код.

Прошейте.

После подачи питания (220В) у вас начнет включаться и выключаться лампочка накаливания. Желтый светодиод LOAD тоже будет светиться, если присутствует нагрузка. В отсутствие нагрузки — не будет светиться.
Проект этого урока полностью вы всегда можете скачать по этой ссылке https://cloud.as.life/s/6fFcfMMrkCofmkL 

Тактирование

Наше устройство тактируется от внутреннего RC генератора на 16 МГц. Внутренний генератор будет тактировать ядро или внешний, мы выбираем в опциях, выставляя соответствующие конфигурационные биты. Посмотреть и изменить эти биты можно прямо в среде разработки, посетив меню ST-Link → Option bytes (если вы используете программатор ST-Link).
Вы увидите там много битов конфигурации, а конкретно CKAWUSEL отвечает за то, какой источник тактирования будет у микроконтроллера. 0 — встроенный RC генератор (по-умолчанию), 1 — внешний генератор или кварцевый резонатор.
По умолчанию, тактовая частота микроконтроллера использовалась у нас 2 МГц, т. к. тактировался камень от внутреннего RC генератора на 16 МГц (HSI), а в настройках делителя по умолчанию стоит максимально возможное деление частоты HSI/8. Для того, чтобы узнать это, я залез в RM:

 

 

Следовательно, чтобы мне начать работать на максимальной частоте 16 МГц, нужно перезаписать значение регистра CLK_CKDIVR, присвоив ему значение 0.

Таймер как источник прерываний

Блокирующая задержка проста и хороша разве что для чего-то очень примитивного, типа Hello, world'а. Для хорошего, неблокирующего маркера времени лучше всего использовать таймер общего назначения и возможность формировать прерывание. Я выберу для этих целей простейший 8-битный таймер TIM4. Можно настроить его так, чтобы он формировал прерывания через определенный промежуток времени.
Прерывание — это кратковременное «выскакивание» из основного кода (в моем случае — из бесконечного цикла). В момент прерывания мы выполняем крошечный объем инструкций и возвращаемся в то место, откуда прервались.
Если говорить очень просто, то мы создадим определенную функцию, которая будет выполняться каждый раз, когда таймер переполнится. А настроим мы его так, чтобы он переполнялся каждую миллисекунду. Следовательно, каждую миллисекунду будет выполняться та самая функция, о которой идет речь. Грамотно такая функция называется «Рутина, обслуживающая прерывание» или Обработчик прерывания.
В заголовочном файле iostm8s003.h вы сможете найти так называемые «векторы прерывания», а точнее их константы. Они записаны со смещением в 2 байта, поэтому значение константы есть порядковый номер вектора прерывания + 2. К примеру, для вектора прерывания №23 — переполнение таймера TIM4, это константа TIM4_OVR_UIF_vector = 25. Вам не нужно писать ручками номера векторов прерываний и уж тем более, их запоминать, просто копипастьте константу. Сейчас мы именно так и поступим при создании рутины прерывания таймера TIM4.

Примерно так создаются все рутины прерываний. Сначала вы обозначаете константу вектора прерываний, а после этого создаете функцию. Имя функции может быть произвольным. Важно при возникновении прерывания от TIM4 программно снимать Event-флаг, иначе из прерывания мы не выйдем никогда — оно будет возникать бесконечно. Флаг этот находится в статусном регистре таймера TIM4. В Reference Manual'е он хорошо описан. Там говорится буквально, что флаг UIF устанавливается аппаратно, при возникновении прерывания (при переполнении таймера, а точнее при его перезагрузке (reload)).

После снятия флага UIF, обратите внимание, я присваиваю переменной t1ms значение 1. Эту переменную необходимо создать в области объявления переменных

Как только возникает прерывание таймера 4 (а возникать оно будет каждую миллисекунду), флаг t1ms будет иметь значение 1 (true). Это будет значить, что прошла 1 мс, а бесконечный цикл (потом) будет постоянно проверять, прошла ли 1 мс.
Теперь нужно настроить таймер TIM4 на то, чтобы он генерил прерывания каждую миллисекунду.
За 1 миллисекунду процессор делает 16000 тактов, и чтобы более менее точно отмерить их, нужно настроить предделитель частоты для таймера с помощью регистра TIM4_PSCR. В него можно записать значение от 0 до 7. Частота тактирования таймера будет равна 16000000 / 2 ^ PSC
8-битный таймер умеет считать только до 255 максимум. Поэтому, чтобы отмерить 16 тыс тактов, нужно разделить тактовую частоту на, как минимум, 62.5 раза. Мы можем разделить на 64. Ведь 2 в степени 6 как раз будет 64. Значит TIM4_PSCR = 6; 
TIM4 перезагружается когда его значение превысит значение регистра TIM4_ARR (Auto-reload register). По умолчанию значение этого регистра = 255. После того как я разделил частоту тактирования TIM4 на 64, я получил частоту тактирования 250000 Гц. И, чтобы 1000 раз в секунду генерировалось прерывание, мне необходимо установить ARR = 249. Так я получу прерывание каждую миллисекунду.
Теперь нужно настроить это программным кодом.

После инициализации таймера можете разрешить глобальные прерывания (по умолчанию они запрещены)

Ассемблерная инструкция rim – разрешает прерывания. А противоположная ей sim – запрещает прерывания глобально.
Этот код, что я/вы написали уже формирует прерывание 

Hello, World - 3

Сейчас я покажу факультативно новый Hello, world. Интервал миганий лампы будет задаваться прерываниями таймера TIM4. Пусть это будет 1 секунда (1000 мс).
Сперва создайте переменную t1sCntr, она поможет отсчитать 1000 прерываний.

Затем создайте новую «ежесекундную» функцию, а внутри нее будет изменяться состояние твердотельного реле (вкл/выкл 220В нагрузки)

В самом main  в бесконечном цикле насчитываем 1 секунду и вызываем ежесекундную функцию. 

Теперь мы можем довольно точно вычислять интервалы и это не будет блокирующая процедура. Мы теперь можем себе позволить некую псевдомногозадачность, ведь теперь у нас есть временной источник событий!
Залейте прошивку, лампочка будет светиться ровно 1 секунду и погасать на 1 секунду.
Скачать весь файл с кодом main.c можно отсюда https://cloud.as.life/s/63TNq2cCA5SLJ6S  

Последовательный порт UART

В Reference Manual'е UART'у посвящено немало текста. Будем читать и вникать.
Расчет бодрейта (скорости), согласно документации

А значение делителя записывается в регистры UART_BRR1, UART_BRR2 кусочками!

Вот такой сюрприз. Читаем дальше, там еще два сюрприза:

Буквально: сначала записывается BRR2 и только после него BRR1, а не наоборот.
А еще значение UART_DIV должно быть не меньше 16. Несложно догадаться, что на частоте 16 МГц максимальный бодрейт составит 1 000 000.
Последовательный порт в этом уроке будет работать на скорости 4800 бод, размер данных будет 8 бит, без контроля четности и с 1 стоп-битом. Для такой скорости UART_DIV будет 3333 (0x0D05). А фактический битрейт получится 4800.48004800.. В RM в таблице производитель все посчитал, кроме 4800. 

По умолчанию, после сброса чипа, регистр BRR1 имеет значение 0, а это подразумевает то, что тактирование UART запрещено. Как только вы записываете правильное значение в BRR1, сразу начинает тактироваться UART.
UART1_CR1 по умолчанию настроен уже удобно: размер данных 8 бит, без контроля четности. Его вообще трогать я не буду.
UART1_CR2 по умолчанию настроен так: отключен Rx, отключен Tx, отключены прерывания от Rx и Tx и разрешать что-то мы будем в самом конце инициализации UART.
UART1_CR3 по умолчанию содержит настройку стоп-битов, 1 шт, как и требуется. Остальные настройки для синхронного порта — мне не интересны. Значит CR3 менять я не буду.
UART1_CR4 предназначен для LIN коммуникации, вообще не интересен.
UART1_CR5 предназначен для SmartCard, и тоже не интересен.
UART1_CR6 тоже про LIN. Не надо.
Значит стратегия инициализации в моем случае проста. Сначала CR1, потом CR2.

Вставьте инициализацию UART так, чтобы она была перед разрешением глобальных прерываний (rim). Еще в коде, перед инициализацией, я добавил прочтение SR и DR в никуда — так мы очистим буфер приема и предотвратим "ложное первое срабатывание" прерывания.
Посмотрите, как IDE подкрасила неиспользуемую переменную temp. Лафхак для перфекционистов: объявите ее так __attribute__((unused)) uint8_t temp; и предупреждений не будет.
И поскольку я разрешил прерывания UART1 RX (по приему байта), я должен создать рутину обработки этого прерывания. По аналогии с созданием рутины для TIM4 делаю еще одну:

Подготовительные работы завершены. Буду теперь передавать данные по UART, а конкретно по RS-485 и смотреть на компьютере, как они передаются.

Hello, World – 4

Для работы с COM портом вы можете использовать любую удобную для вас программу. Если не знаете какие бывают, приведу пару примеров: Putty, Terminal v1.9b, Doclite. 
Для работы с COM портом вам потребуется преобразователь USB ↔ RS-485.
Вот так будет выглядеть результат работы микроконтроллера в терминале UART:

Нет ничего проще передачи по UART, именно поэтому проверку UART я начну именно с передачи. А для этого нужно написать простую блокирующую функцию побайтовой передачи и в ежесекундную функцию добавьте задачу на передачу строки.
Поскольку работаем мы с портом RS-485, то я обязан прояснить, что по этому интерфейсу прием и передача информации ведется в полудуплексном режиме. Это напоминает принцип работы рации: нажали кнопку PTT – говорите, отпустили PTT – слушаете. При этом невозможно одновременно слушать и говорить. Микросхема приемопередатчика RS-485 устроена как раз для возможности работать в полудуплексном режиме — у нее есть вывод DE. Когда на DE подается логическая единица, то микросхема работает как передатчик, а если на DE логический ноль, то микросхема работает как приемник.
Внутри написанной функции UDE_SET = 1 включает передатчик и немного ждет, т. к. этот процесс может занимать некоторое время. А дальше я записываю в DataRegister DR байт, дожидаюсь что регистр опустошился и так далее, байт за байтом передаю весь буфер.
В самом конце, когда у меня опустошился DR после записи последнего байта, я не спешу отключать передатчик – по порту все еще идут данные! После установки флага TXE порт передаст еще 10 бит в течение примерно 2 мс (на скорости 4,8 kbps). Примитивная задержка 1000 подобрана экспериментально. С задержкой меньше 970 последний байт не передается до конца.

Modbus RTU

Если вы совершенно не знакомы с этим промышленным протоколом, то рекомендую прочитать о нем как можно больше в интернете. От себя лишь коротко скажу: Modbus RTU – это когда один мастер обращается к многочисленным подчиненным, дабы прочитать из них информацию или передать свою. У каждого подчиненного (слэйва) есть свой уникальный ID в сети, в это значит, что отдельное обращение может быть адресовано конкретному узлу в поле.
MERSON 24A2 это исполнительное устройство, созданное для управления клапанами полива газона. В «поле» может находиться много таких устройств и у каждого из них свой уникальный сетевой адрес (Slave ID). Modbus RTU – это манипуляция массивами 16-битных регистров. Каждый такой массив называется «таблицей» Modbus.
Я буду использовать только один массив и он будет доступен для чтения через функцию 0x03, доступен для записи через функции 0x06 и 0x0A.
Этот массив я буду представлять через структурный тип, что на мой взгляд очень удобно.

Тренировки и Hello, World'ы кончились. Сейчас мы перейдем к созданию конечного устройства. 

Создайте новый файл, обзовите его main.h и заполните по образцу.

Для работы Modbus протокола я добавлю к проекту уже готовые файлы modbus.c и modbus.h, которые я написал достаточно давно. Я не стану объяснять как работает стек Modbus RTU, так как это совершенно не относится к специфике разработки для STM8. Стек будет работать практически на любом микроконтроллере. Просто заберите файлы modbus.h и modbus.c из архива с проектом (ссылка будет в конце этой статьи) и поместите рядом с main.c.
В main.h, как видите, я создаю структурный тип DEV_REGS, но переменную DEV_REGS regs я объявляю внутри modbus.c. Через эту структуру мы будем общаться с устройством по Modbus, она наши глаза, уши и самый лучший отладчик (будет).
Внутри же самого main.c редактируйте подключаемые заголовочные файлы

Обратите внимание, что здесь больше нет #include <iostm8s003f3.h> и #include <stdint.h>, т. к. они ушли в main.h.
Далее, для обработки входящих датаграмм по UART я создам еще несколько переменных и констант.

Массив значений, он же таблица Holding Registers я экстерню из modbus.c. Служебное слово extern позволяет использовать одни и те же (нестатичные) переменные и функции, объявленные в других модулях.
Привожу в порядок рутину обслуживания прерывания UART RX

В ежесекундных событиях делаю обработку реле времени включения нагрузки. У меня есть регистр outCfg – это буквально таймер работы нагрузки. Если он равен 0 — нагрузка отключается. Если он больше 0 — нагрузка включается, однако этот таймер уменьшается на 1 каждую секунду. Есть исключение, если значение таймера максимально возможное (0xFFFF), то он не будет уменьшаться, а нагрузка будет включена постоянно до тех пор, пока значения таймера не поменяют на 0 извне.

Каждую миллисекунду программа будет отслеживать, не ждем ли мы байта по UART'у. Я сделал такой алгоритм: если по UART принимается байт, то счетчик таймаута uartTailCntr обнуляется и начинается отсчет. Каждый новый байт обнуляет этот таймаут. Как только байты перестали приходить, выходит таймаут, допустим 20 мс. Значит новых данных мы не ждем от мастера, начинаем обрабатывать те байты, что уже приняты в линейный буфер.
Запрещаем прерывания Rx, чтобы буфер не побить. Парсим этот буфер, в него же записываем ответ, отправляем ответ. Разрешаем прерывания Rx обратно.

И последний момент. У устройства же должен быть сетевой адрес. Modbus стек опирается на значение regs.slaveID. Пока у нас не сохраняются настройки в EEPROM, я временно примухлюю SlaveID = 1;

Теперь можно откомпилироваться, зашиться и попробовать работу Modbus на любимом Modbus клиенте. Я использую Modbus Poll. Прикрепляю проект и там же файл конфигурации Modbus для программы Modbus Poll https://cloud.as.life/s/Sz9ejfP2dffXpbb 

 

АЦП

Аналогово-цифровой преобразователь предназначен для измерения (оцифровки) величины напряжения на канале. У меня в устройстве есть два таких канала, которые нужно оцифровывать поочередно, а вычисленные значения преобразовывать в напряжение и ток.
В STM8S содержится 10-битный АЦП. Это означает, что результатом измерения напряжения на канале будет значение от 0 до 1023, где 0 — полное отсутствие напряжения, 1023 — напряжения на канале выше или равно напряжению на выводе VREF+ (на STM8 в маленьких корпусах он подключен к VDD).
На устройстве MERSON 24A2 используются 2 канала АЦП, которые подключены к аналоговым измерительных входам.

Напряжение на входе 1 проходит через делитель R9-R15 и поступает на канал AIN3. Аналогично, напряжение входа 2 проходит через делитель R10-R14 на канал AIN4. Номиналы резисторов на делителе одинаковы, значит входное напряжение делится пополам, а мы способны измерить от 0 до (2 * VDD). Приблизительно VDD в этом устройстве составит 5.2 Вольта. Таким образом диапазон измеряемых напряжений на входах от 0 до 10.4 Вольта.
Так же, мы можем нагрузить каналы шунтами R12, R13 через транзисторы Q2, Q1 и тогда, по напряжению на шунтах мы можем достоверно вычислить ток, которым эти шунты нагружены. В промышленной автоматике обычно используется токовый диапазон 4 — 20 мА. Мы знаем диапазон измеряемых напряжений, от 0 до 10.4 Вольт. По закону Ома нетрудно вычислить диапазон измеряемых токов: I = 10.4 / 220 = 47.3 mA максимум.
Линии портов ввода-вывода, где присутствуют нужные измерительные каналы AIN3 (PD2) и AIN4 (PD3)  настроены как входы без подтяжки, так называемые floating inputs. Это значит, что мы без проблем сможем цепляться к ним программно мультиплексором АЦП в любой момент времени. 
Но сначала сконфигурируем сам АЦП:
Откройте Reference Manual и посмотрите в раздел конфигурации АЦП

Конфигурационный регистр CR1 позволяет выбрать предделитель частоты тактирования АЦП. Я выбираю самый медленный /18.

Выравнивание данных я выберу ДО включения АЦП (а он по умолчанию выключен, т. е. бит CR1 → ADON равен 0). В регистре CR2 есть такой бит ALIGN

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

После окончания измерения результат запишется в 2 регистра ADC_DRH (старший) и ADC_DRL (младший). Читать регистры АЦП нужно в строгой последовательности, в зависимости от выбранного выравнивания. Для Right Align читаем сначала ADC1->DRL, потом ADC1->DRH. Для Left Align наоборот.
Теперь приступим к реализации кода. 
У STM8 достаточно неплохой модуль АЦП, не смотря на скромное разрешение в 10 бит. Чаще всего достаточно сделать один замер и по нему определить величину напряжения на АЦП. Но в реальном мире при наличии или отсутствии совокупности разных кондуктивных помех на вход могу проскакивать «кривулины» и показания начнут скакать. Чтобы этого не произошло, я делаю несколько замеров (сэмплов) и усредняю все полученные значения, а это существенно повышает точность. Кроме того у нас есть возможность проводить сколь угодно большое количество замеров не блокируя процессор на время измерения — многократно вызывать функцию, которая будет управлять замерами. Вот она:

Это собственно и есть вся работа с АЦП в режиме одиночных замеров. Все остальное будет написано лишь для иллюстрации того, как этот АЦП применяется в моем устройстве. 
Для точных измерений мне необходимо достоверно знать напряжение питания микроконтроллера, ведь именно по нему я масштабирую результат. В схеме я не применял источника опорного напряжения, а создал регистр настроек, в который при сборке устройства монтажник будет записывать измеренное значение напряжения питания микроконтроллера. Таким образом таблица регистров немного изменится:

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

А в области глобальных переменных объявлю несколько переменных, предназначенных для измерения аналоговых каналов.

Вес единицы АЦП — это величина, которая соответствует единице сырого значения АЦП.  Она обновляется раз в секунду, и возможно, в этом нет необходимости, но при изменении уставки (настройки) напряжения питания VDDA, adcWt должна пересчитаться.
В ежесекундные события я добавляю пересчет величины adcWt. Так же добавляю туда включение и выключение шунтов, добавляю обновление бита наличия нагрузки 220В.

Далее я добавляю несколько специфических для устройства функций вычисления напряжения / тока. Тут фигурируют такие константы: 2 — напряжение на входе в 2 раза выше чем напряжение на канале АЦП; 0.22 — сопротивление шунта в килоОмах.

И наконец, внутрь функции main() в бесконечный цикл я добавлю обработку замеров АЦП.
Каждый раз, в бесконечном цикле, мы обращаемся к функции GetADCValue(), которая возвращает 0, когда замеры еще не окончились или 1, если замеры прошли и пора забирать результат. Благодаря тому, что GetADCValue() мгновенно сообщает, готов результат или еще нет, мы не блокируем программу на время работы с АЦП. 
В зависимости от текущего канала и битовой карты конфигурации шунтов мы вычисляем актуальные величины напряжения/тока на каждом аналоговом канале и обновляем карту дискретных входов.
После обработки одного канала, мы переключаемся на другой, и так поочередно.

Весь проект целиком вы можете скачать отсюда https://cloud.as.life/s/AfdEN5Gx8C43SQS  

Чтение и запись EEPROM

Можно читать и записывать область EEPROM двумя способами:
1) Обращаясь напрямую по физическим адресам области памяти EEPROM
2) Создав массив данных с модификатором EEPROM и работая только с массивом
На мой взгляд, второй способ является самым привлекательным, так как, во-первых это очень удобно, а во-вторых, можно создать предустановленные (заводские) значения EEPROM, которые запишутся в эту область при прошивке устройства.

Чтобы записать что-то в EEPROM вы просто записываете это в массив.
IAR создали интересный механизм обращения к массиву с модификатором EEPROM, при попытке записать что-то в этот массив, будут вызываться пользовательские функции-посредники. Если этих функций не будет, линкер заругается.

Перед тем как записывать данные в массив EEPROM необходимо разблокировать FLASH память. Процедуры блокировки и разблокировки здесь:

И, наконец, процедуры записи

Особенность этих процедур в том, что запись происходит только в том случае, если данные отличаются. Количество циклов перезаписи EEPROM в STM8S003 минимум 10000 раз, а в STM8S103 минимум 100000 раз. Поэтому не нужно перезаписывать данные без нужды.
Скачать проект с исходниками можно отсюда https://cloud.as.life/s/5pDep7TzAxZ3sY4 

Обсудить эту статью в Telegram >> клик <<

Tags: ,