Руководство по stm8

разделы: STM8 , АССЕМБЛЕР , дата: 4 марта 2018г.

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

Наибольшие вопросы в STM8 у меня вызывал 3-уровневый конвейер, поэтому в основу перевода легла глава, рассмативающая работу этого конвейера.

Если сравнивать систему команд STM8 с ассемблером AVR, то здесь имеется большое количество 16-битных команд, целочисленное умножение и деление, условные переходы объединены с проверками, не надо вечно перепрыгивать через команду. Имеются полноценные битовые команды, которые выполняются за один цикл. В целом, мне показалась, что писать на ассемблере STM8 можно также легко как на Си.

    Содержание::

  1. Описание ядра STM8

    1. Введение
    2. Регистры ЦПУ
  2. Интерфейс памяти STM8

    1. Программное пространство
    2. Пространство данных
    3. Архитектура интерфейса памяти
  3. Принципы работы конвейера

    1. Описание уровней конвейера
    2. Этап выборки (fetch)
    3. Декодирование и вычисление адреса
    4. Этап исполнения
    5. Конфликты на шине данных
    6. Примеры работы конвейера
    7. Предварительное соглашение
    8. Пример оптимизированной работы конвейера — выполнение программы из флеш-памяти
    9. Пример оптимизированной работы конвейера — выполнение программы из ОЗУ
    10. Пример работы конвейера с инструкциями перехода JP и CALL
    11. Приостановка конвейера
    12. Работа конвейера с ожиданием в один цикл

3 Описание ядра STM8

3.1 Введение

ЦПУ имеет полностью 8-битную архитектуру с 16-битными операциями
на индексных регистрах (для вычисления адресов). Шесть внутренних
регистров позволяют эффективно оперировать с 8-битными данными.
Система команд состоит из 80 базовых инструкций ЦПУ.
20 режимов адресации позволяют адресовать к 6 внутренним регистрам
и 16 Мбайтам оперативной памяти и/или регистрам ввода-вывода.

3.2 Регистры ЦПУ

Шесть регистров ЦПУ показаны на рисунке 1. Перед входом в обработчик прерывания
содержимое этих регистров сохраняется в стеке в порядке, показанном на рисунке 2.
При возвращении из обработчика прерывания содержимое регистров восстанавливается.

Аккумулятор

Аккумулятор — это главный 8-битный регистр, используемый для хранения операнда
и результата арифметических и логических операций, а также для операций с
данными.

Индексные регистры (X и Y)

Это 16-битные регистры, используемые для вычисления адреса или для хранения
временных значений при операциях с данными. В большинстве случаев компилятор
генерирует прекод инструкции (PRE) для обозначения того, что следующая инструкция
ссылается на Y регистр. Оба регистра X и Y автоматически сохраняются при возникновения
прерывания.

Программный счетчик (PC)

Программный счетчик — это 24-битный регистр для хранения адреса следующей
инструкции. Он автоматически обновляется после выполнения каждой инструкции ЦПУ.
Таким образом STM8 может адресовать 16-Мбайтное адресное пространство.



рисунок 1. Регистры ЦПУ


Указатель стека (SP)

Указатель стека — это 16-разрядный регистр. Он содержит адрес следующей
свободной ячейки стека. В зависимости от модели микроконтроллера, старшие биты
указателя могут быть аппаратно предустановлены в то или иное значение.

Стек используется для сохранения содержимого регистров ЦПУ при вызове подпрограммы
или возникновения прерывания. Пользователь также может напрямую использовать
стек через инструкции POP и PUSH.

После подачи питания на микроконтроллер, указатель стека устанавливается
в наибольшее допустимое значение. Значение регистра уменьшается при помещении
данных в стек и увеличивается при извлечении их оттуда. Когда указатель достигнет
нижней допустимой границы, его значение снова будет установлено в максимально допустимое значение.
Тогда ранее сохраненные данные будет переписываться и, следовательно, они будут
потеряны.

Вызов подпрограммы занимает две или три ячейки стека.

При возникновении прерывания регистры ЦПУ(CC, X, Y, A, PC) сохраняются
в стеке. Эта операция занимает 9 тактов и использует 9 байт ОЗУ.

Примечание. Режимы WFI/HALT сохраняют содержимое регистров заранее. Т.о., при
возникновении прерывания скорость реагирования на него увеличивается.



рисунок 2. Сохранение и восстановление содержимого регистров ЦПУ при возникновении прерывания

Глобальный конфигурационный регистр (CFG_GCR)

Глобальный конфигурационный регистр привязан к адресу в оперативной памяти.
Он управляет конфигурацией процессора и включает в себя AL-бит:

AL — уровень активности(activation level)

Если AL сброшен в ноль, то инструкция IRET (выход из обработчика прерывания) восстановит содержимое регистров из
стека, и основная программа продолжит выполнение со следующей инструкции после
WFI.

Если AL установлен в единицу (когда работают только прерывания), то IRET вернет
ЦПУ обратно в режим WFI/HALT, без извлечения содержимого регистров из стека.

Этот бит отвечает за пониженное энергопотребление микроконтроллером. В режиме пониженного
энергопотребления микроконтроллер большую часть времени проводит в режимах WFI/HALT, изредка
пробуждаясь (через прерывания) для выполнения своих задач. Эти задачи могут быть настолько
короткими, что могут выполняться непосредственно в обработчике прерывания, без возвращения
в основную программу. В таком случае флаг AL устанавливается в 1 переводом микроконтроллера
в режим пониженного энергосбережения (с помощью инструкций WFI/HALT). В результате
время выполнения обработчика прерывания уменьшается за счет того, что содержимое регистров ЦПУ
не сохраняется и не восстанавливается каждый раз при возникновении прерывания.

Статусный регистр (CC)

Статусный регистр — это 8-битный регистр, в который заносятся флаги результата только что выполненной
инструкции ЦПУ. Эти флаги могут быть проанализированы независимо друг от друга специальными
инструкциями, в результате чего могут быть приняты те или иные действия.

● V: флаг переполнения (overflow)

Если V флаг установлен, то это показывает, что при выполнении последней знаковой арифметической операции старший бит — MSB — был установлен в единицу. Для подробностей смотрите описание инструкций: INC, INCW, DEC, DECW, NEG, NEGW, ADD, ADC, SUB, SUBW, SBC, CP, CPW.

● I1: маска прерывания первого уровня

Этот флаг работает совместно с флагом I0 и определяет текущий уровень прерывания согласно нижеследующей таблице. Эти флаги могут быть установлены программно следующими инструкциями: RIM, SIM, HALT, WFI, IRET, TRAP и POP. Или они могут быть установлены автоматически аппаратным способом при входе в обработчик прерывания.

● H: флаг полупереноса

Этот флаг устанавливается в 1, когда произошел перенос из 3-го бита в 4-й при выполнении инструкций ADD или ADC. Флаг H используется при работе с BCD-арифметикой. В случае ADDW, SUBW этот флаг устанавливается, когда произошел перенос между 7-м и 8-м битами индексных 16-битных регистров.

● I0: маска прерывания нулевого уровня

см. флаг I1.

● N: флаг отрицательного числа

Флаг устанавливается в 1, когда последняя арифметическая, логическая или операция с данными привела к появлению отрицательного числа (т.е. старший бит был установлен в единицу).

● Z: флаг нуля

Флаг устанавливается в 1, когда последняя арифметическая, логическая или операция с данными привела к появлению нуля.

● C: флаг переноса

Флаг переноса — «C» устанавливается тогда, когда в результате последней арифметической
операции в АЛУ со старшим битом (MSB) произошел перенос или заимствование. Этот бит также
участвует в операциях проверки, ветвления, сдвига и загрузки. Смотрите описания инструкций:
ADD, ADC, SUB, SBC для подробностей.

В операциях битового тестирования инструкции работают с копией флага «C». Смотрите описания
инструкций: BTJF, BTJT для подробностей.

В операциях сдвига «C» флаг будет обновлен. Смотрите описания инструкций: RRC, RLC, SRL, SLL, SRA для подробностей.

Флаг может быть программно установлен, сброшен, или может изменить значение на противоположное
с помощью инструкций: SCF, RCF, CCF.

Пример: сложение

Влияние выполнения каждой инструкции на статусный регистр отображено в таблице инструкций STM8. Для примера:

Где:

N — не влияет;

Имя флага — влияет;

1 — устанавливается в единицу;

0 — устанавливается в ноль.

4 Интерфейс памяти STM8

4.1 Программное пространство

Программное пространство равно 16 Мбайт и оно линейно. Отличия 1-, 2-, и 3-байтных режимов
адресации показаны на рисунке 3.

● «Страница» [0xXXXX00 до 0xXXXXFF] — это 256-байтный участок памяти с одинаковыми двумя старшими
байтами (ХХХХ определяют номер страницы).

● «Секция» [0xXX0000 to 0xXXFFFF] — это 64КБ участок памяти с одинаковым старшим байтом
(ХХ определяют номер секции).

Reset и таблица векторов расположены по адресу 0х8000 для семейства STM8. (Примечание:
для последующих серий адрес может быть изменен.) Таблица векторов имеет 32 4-байтных
записей: RESET, Trap, NMI и 29 векторов нормальных пользовательских прерываний. Каждая запись
содержит опкод 0x82 и следующее за ним 24-битное значение: PCE, PCH, PCL — адрес расположения
обработчика прерывания. Главная программа и обработчики прерываний могут располагаться в любом
месте 16МБ пространства.

Инструкции CALL/CALLR и RET должны использоваться только в пределах одной секции.
CALL/RET использует смещение относительно текущего значения PCE регистра.
Для JP адрес перехода должен быть длиной 16 или 17 (при индексной адресации) бит.
Это значение добавляется к текущему значению PCE. Чтобы переместиться на любой адрес
программного пространства, инструкции перехода JPF и CALLF используют расширенную
адресацию с тремя байтами в качестве адреса. В то же время RETF возвращает из стека
три байта адреса возврата.

Т.к. программное пространство линейно, секции могут пересекаться двумя способами: выполнением
следующей инструкции (PC+1), относительным переходом, и, в некоторых случаях, инструкцией JP
(с использованием индексной адресации).


Примечание: в целях безопасного использования памяти, функции, пересекающие границы секторов,
ОБЯЗАНЫ:
— вызываться инструкцией CALLF;
— включать в себя инструкции только с расширенной адресацией(CALLF и JPF)

Все метки относятся к нулевой секции (пример: JP [ptr.w], — где ptr.w расположен в нулевой
секции, а JP адресуется к текущей секции).

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

4.2 Пространство данных

Размер пространства данных составляет 16 Мбайт. Т.к. стек расположен в нулевой секции,
доступ к данным за пределами нулевой и первой секции осуществляется только инструкцией LDF.
Для максимальной эффективности вашей программы часто используемые данные должны
располагаться в нулевой секции.

Все метки данных расположены исключительно в нулевой секции.

Индексная адресация (16-битный индексный регистр в сочетании с длинным смещением) позволяет
адресовать нулевую и первую секцию.

Вся периферия имеет собственные адреса в адресном пространстве.

4.3 Архитектура интерфейса памяти

STM8 имеет гарвардскую архитектуру с независимыми шинами для программы и для данных.
Однако, логически, адресное пространство объединено через интерфейс памяти в одну адресную шину
размером в 16 Мбайт с непересекающимися участками. Интерфейс памяти показан на рисунке 4.
Он состоит из двух шин: шины исполняемого кода и шины данных, сигнала переключения режима доступа
в чтение-запись (R/W) и сигнала подтверждения(STALL).

Сигнал подтверждения STALL делает ЦПУ совместимым с медленными последовательными и
параллельными интерфейсами памяти. Когда интерфейс памяти работает медленно,
ЦПУ ожидает от интерфейса подтверждения перед тем как выполнять инструкцию. В таком случае
время выполнения инструкции увеличивается до значения, указанного в этом руководстве.

Шина исполняемого кода является 32-битной. Это позволяет большинству инструкций выполняться за
один цикл.

Поскольку все адресное пространство объединено в общую 24-битную шину, то данные могут быть
расположены в области программы (флеш-памяти), а программный код может выполняться из
оперативной памяти. В последнем случае пострадает быстродействие, т.к. программный
код и данные будет получаться из одной шины, которая за один такт может получить только
один байт. Следовательно, время выполнения инструкции будет больше.

5 Принципы работы конвейера

Семейство микроконтроллеров STM8 имеет 3-уровневый конвейер для ускорения выполнения инструкций.
Конвейер позволяет выполнять несколько операций одновременно, в отличии от их традиционного
последовательного выполнения.
Операции конвейера состоят из:

  • Выборки
  • Декодирования
  • Исполнения

Программный счетчик всегда указывает на инструкцию на этапе декодирования, как показано на рисунке 5:

5.1 Описание уровней конвейера

Рисунок 6 и глава 5.1.1, глава 5.1.2 и глава 5.1.3 предоставляют исчерпывающую информацию
о работе каждого уровня конвейера.

5.1.1 Этап выборки (fetch)

Первый уровень работы конвейера включает в себя 64-битный буфер выборки и 32-битный
предварительный буфер. Все вместе они составляют три машинных слова, называемых F1, F2 и F3.
Структура буфера позволяет загрузить любую инструкцию (более 5 байт) через F1
(и F2, если необходимо) для немедленного декодирования.

Инструкции, получаемые из флеш-памяти, имеют 32-битный размер и выравниваются по
границе, кратной четырем байтам, т.е. 0хXXX0, 0xXXX4, 0xXXX8, или 0xXXXC и т.д.

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

Чтение инструкции из ОЗУ походит на чтение из ПЗУ. Однако, вследствие того, что шина ОЗУ
является лишь 8-битной, потребуется 4 последовательных операции чтения, чтобы заполнить одно
Fx слово. Вследствие этого код из ОЗУ выполняется медленнее, нежели с флеш-памяти.

5.1.2 Декодирование и вычисление адреса

Этап декодирования включает в себя операцию по выравниванию инструкции. Модуль выравнивания
использует 64-битный буфер выборки, извлекает из него инструкцию и подает ее в модуль декодирования.

    Инструкция состоит из двух частей:

  • опкода (1 или 2 байта);
  • данных или адреса (от 0 до 3-x байт).

На этом этапе декодируется опкод. Если присутствует адресная инструкция,
то сначала вычисляется непосредственный адрес, а если присутствует непосредственный операнд,
то он отправляется сразу на выполнение.

Длинные и невыровненные по границе 32 бит инструкции

Для длинных инструкций (более 5 байт) выборке необходимо дважды получать доступ к памяти,
для загрузки инструкции с операндом(и). В этом случае этап декодирования приостанавливается
(после декодирования опкода) в ожидании, пока выборка завершит работу.

В случае с короткими инструкциями такая ситуация может возникнуть, когда инструкция
пересекает 32-битную границу.

Косвенная адресация

В случае косвенной индексации ЦПУ приостанавливает работу в ожидании пока считается
указатель из памяти данных (т.е. из ОЗУ). Число пропускаемых при этом циклов зависит
от длины указателя (короткий, длинный или расширенный).

5.1.3 Этап исполнения

На этапе исполнения инструкция выполняется, а результат заносится в аккумулятор, индексный
регистр или в ОЗУ.

5.2 Конфликты на шине данных

Три типа операций получают доступ к памяти данных:

  • вычисление адреса в случае косвенной адресации;
  • операция чтения: получение исходного операнда;
  • операция записи: сохранение или операция чтения-модификации-записи.

В случае одновременного доступа к одной и той же области памяти обоих уровней выполнения
конвейера: для записи (при выполнении) и для чтения (при декодировании), операция декодирования
приостанавливается, пока этап выполнения не освободит ресурс.

5.3 Примеры работы конвейера

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

5.4 Предварительное соглашение

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

  • Будем считать, что для этапа декодирования требуется только один цикл;
  • Этап декодирования занимает несколько циклов, равных:
  • Cy = DecCy + ExeCy — 1

где:

Cy — количество циклов используемых для выполнения инструкции. В случае декодирования и выполнения,
это соответствует минимальному количеству циклов необходимых для выполнения инструкции,
и не учитывает влияние от последовательности инструкций.

DecCy — точное количество циклов декодирования.

ExeCy — точное количество циклов выполнения.

Этап декодирования следующей инструкции начинается на последнем цикле этапа выполнения
предыдущей инструкции. В случаях инструкций выполняющих сброс очереди конвейера, соглашение
состоит в том, что выборка следующей команды начинается в последний цикл этапа выполнения
предыдущей инструкции.

Точное количество циклов (см. таблицу 3) и количество циклов полученных при использовании
этого соглашения (см. таблицу 4) одинаково.

5.4.1 Пример оптимизированной работы конвейера — выполнение программы из флеш-памяти

В примере показанном в таблице 6, исполняемый код извлекается из флеш-памяти через
32-битную шину. Т.е., необходимо три цикла для заполнения всего 96-битного буфера выборки.
Каждый цикл загружает одно машинное слово в регистры буфера выборки: F1, F2, F3. Следующая
операция выборки для регистра Fx
сможет стартовать только когда все инструкции содержащиеся в нем будут декодированы.
По таблице видно, что только в 9-ом цикле последняя инструкция (SWAP A) содержащаяся в регистре
F3 будет декодирована и операция выборки сможет загрузить в F3 новое значение.

5.4.2 Пример оптимизированной работы конвейера — выполнение программы из ОЗУ

В примере показанном в таблице 8, программа считывается из ОЗУ через 8-битную шину.
В этом случае требуется 12 циклов чтобы заполнить 96-битный буфер выборки.
Каждые 4-е цикла, одно машинное слово загружается в регистр Fx. Декодирование первой
инструкции регистра Fx начинается только тогда, кода это регистр полностью загружен.
В примере это происходит на 4-м цикле, и первая инструкция (NEG A) начинает выполняться только
на пятом цикле.

В случае необходимости чтения или записи в ОЗУ, этап выборки может быть приостановлен.
Это происходит на 6-м цикле, когда требуется получить содержимое адреса ОЗУ 10 для
декодирования инструкции XOR A,$10.

5.4.3 Пример работы конвейера с инструкциями перехода JP и CALL

В примере данном в таблице 10, выполняются переходы после инструкций JP/CALL. Тогда кеш инструкций
содержащийся в буфере выборки теряется (происходит очистка буфера, т.н. «Flush»), и его требуется заполнить новыми инструкциями.
Начало выборки зависит от выполняемой инструкции.

Для инструкции перехода JP, выборка начнется во время последнего цикла выполнения инструкции.

Для инструкции вызова подпрограммы CALL, она стартует только после полного выполнения инструкции.

5.4.4 Приостановка конвейера

Этап декодирования может быть приостановлен, если выполнение длится более одного цикла.

Очистка очереди конвейера происходит в случае перехода. Выборка адреса перехода
осуществляется во время второго цикла инструкции BTJF.

Этап декодирования также может быть приостановлен когда используемая память или регистр
изменяются во время выполнения предыдущей инструкции. В примере данном в таблице 12,
инструкция INCW Y записывает в X регистр в течении первого цикла выполнения. В итоге, следующая
инструкция LD A,(X) не может прочитать источник в X регистре.

5.4.5 Работа конвейера с ожиданием в один цикл

В примере приведенном в таблице 14, выборка выполняется за 2 цикла, и
выборка в регистры не перекрывается(по строке таблицы).

Если инструкция декодируется / выполняется в течение последних 2 циклов выборки,
то состояние приостановки приравнивается к состоянию выполнения.

Уровень сложности
Простой

Время на прочтение
6 мин

Количество просмотров 8.1K

Речь пойдет о маломощном микроконтроллере STM8L001J3. Постараюсь подробно рассказать о необходимых шагах для прошивки «hello world» и различных подводных камнях.

Основные характеристики микроконтроллера:

Максимальная частота ядра

16 МГц

Объем памяти программ

8 кБайт

Объем EEPROM

256 Байт

Объем оперативной памяти

1.5 кБайт

Количество входов/выходов

6

Поддерживаемые интерфейсы

SPI, I2C, UART

Напряжение питания

1,8..3,6 V

Корпус

SO8

Из этих данных видно, что STM8L001J3 вполне может заменить так горячо любимый всеми ATTiny13. Более подробно можно посмотреть на официальном сайте STM.

Для изучения нам понадобятся:

  1. Вот такой девайс — Модуль STM8L, он минималистичен, но все необходимое на борту имеется;

  2. IDE, приложение для написания и редактирования кода; после долгих проб остановился на IAR EW for STM8;

  3. Ну и, конечно, программатор ST-Link V2. Любой клон, главное чтобы был рабочий.

Часть 1. Установка и настройка IAR EW for STM8

Помимо этого варианта есть и другие возможные комбинации работы с микроконтроллером, например, Code::Blocks/SDCC/ST Visual Programmer или STVD/Cosmic/STVP. Кстати, «Cosmic» на мои письма не ответила по неизвестной причине. Мы же будем использовать IAR EW for STM8.

На сегодняшний день есть масса возможных ограничений, которые можно обойти, затратив некоторые усилия. Поэтому я буду освещать только общие моменты. Возможно, когда Вы будете читать эту статью что-то изменится. Скачиваем IAR EW for STM8 с официального сайта (не обязательно) последнюю версию и ставим на компьютер. Дальше настраиваем среду для работы. Маленькое отступление для тех, кто любит видеоуроки: есть очень хорошие обзоры у Бородатого Инженера. Там, правда, простая STM8, но первые уроки вполне подходят. А здесь расскажу в картинках, как это все настроить.

Открываем приложение и заходим в File/New Workspace, тем самым создаем новое рабочее пространство. Сохраняем проект, называем, к примеру «HW-STM8L»; нажимаем «ОК». Получаем вот такой экран:

Добавляем файлы библиотек в проект. Для этого создадим две новых группы, ПКМ (правая кнопка мышки) по корню, Add/Add Group, даем им имена «Inc» и «src».

Должно получиться так
Должно получиться так

Далее внимательно: если Вы параллельно смотрите видео, то будут отличия. Файлы библиотек SPL для STM8 и STM8L различные. Поэтому идем на официальный сайт ST и скачиваем библиотеки для нашей 8L. Называется файл «en.stsw-stm8012». Сохраняем на диске и разархивируем. Теперь, чтобы добавить файлы библиотек в наши вновь созданные папки, кликаем ПКМ по этим папкам, Add/Add Files. Находим на диске разархивированный файл en.stsw-stm8012\STM8L10x_StdPeriph_Lib\Libraries\STM8L10x_StdPeriph_Driver выбираем всё и добавляем в папки соответственно их названиям.

Теперь добавляем основной файл и файлы конфигурации. Находим разархивированный файл библиотек. Выбираем из папки en.stsw-stm8012\STM8L10x_StdPeriph_Lib\Project\STM8L10x_StdPeriph_Templates четыре файла и переносим их в папку с проектом.

Соглашаемся с заменой, «ОК». Для удобства перенесем эти файлы в корень. Для этого опять ПКМ на корне и добавляем три файла (mane.c уже поменялся):

Продолжаем настраивать конфигурацию: ПКМ по корневому файлу, далее Options. Появляется вот такое окно, выбираем в нём наш микроконтроллер:

Выбираем наш микроконтроллер

Выбираем наш микроконтроллер

Следующая настройка С/С++ Comiler/Preprocessor, туда добавим наши папки «inc» и «src».

Также добавляем папку с четырьмя добавленными файлами.

Прописываем наш микроконтроллер в это окно Defined:

Далее в этом окне идем в Debugger меняем Simulator на ST-Link. Следом в Output Converter: ставим галку и выбираем Output format как «Intel Extended».

Нажимаем «ОК» и ждём, пока настройки применятся. Потом F7 или зеленый многогранник со стрелочкой, именуем Work Space, «ОК», даем какое-то время для сборки. После сборки ошибок быть не должно. Если они есть, то исключите файл, на который жалуется приложение из папки «src». Примерно так: ПКМ на файле, «Options» и ставите галку здесь:

В моем случае жалоба была на файл «stm8l10x_itc.c», после его исключения ошибка пропала. Вот он, наш замечательный бесконечный цикл. Настройка завершена.

Часть 2. Разбираем Модуль STM8L

На борту этого модуля микроконтроллер, светодиод и кнопка. Питается от внешнего напряжения 5-15 Вольт. Есть отдельные пины для подключения программатора. При необходимости можно удалить перемычки J1 и J2, тем самым освободив пины микроконтроллера под свои нужды. В комплекте, кстати, идет дополнительно еще один микроконтроллер и макетка.

Module STM8L

Module STM8L

По схеме у нас светодиод на выводе №6 — PB6, а кнопка на выводе №7 — PB7, это нам понадобится в третье главе, когда будем писать код для микроконтроллера.

Также обратите внимание: здесь всего три вывода для внутрисхемного программирования. Отсутствует привычный четвертый вывод «Reset». У этого микроконтроллера его нет. Эту особенность, нужно учитывать при программировании, и она накладывает некоторые ограничения при написании кода. Подробнее в следующей главе.

Часть 3. Пишем код и заливаем

Прежде всего необходимо рассказать о главной особенности микроконтроллера STM8L001J3. Я уже писал, что разработчики отказались от «Reset» ножки. Поэтому в даташите рекомендуется при написании кода в первых же строках дать 5-секундную задержку перед началом выполнения основной программы. Таким образом, при подаче питания у нас есть 5 секунд для начала заливки кода. Если заливки нет, программа благополучно начнет работу после небольшой паузы. Когда же код будет полностью отлажен, можно убрать задержку и прошить, осознавая, что внести изменения кода в микроконтроллер больше нельзя.

Итак, полагаясь на даташит и знания Си, пишем наш Hello World:

/**
  ******************************************************************************
  * @file     Project/STM8L10x_StdPeriph_Templates/main.c
  * @author   KHod
  * @version V1
  * @date    20-JNR-2023
  * @brief    This file contains the firmware main function.
  ******************************************************************************
  *
  *        https://elmodule.sytes.net/?p=130
  *
  *
  ******************************************************************************
  */

/* MAIN.C file */
#include "stm8l10x.h"
#include "stm8l10x_gpio.h"
#include "stm8l10x_clk.h"
#define ASM asm

/* This delay should be added just after reset to have access to SWIM pin
 and to be able to reprogram the device after power on (otherwise the
 device will be locked) */
#define STARTUP_SWIM_DELAY_5S \
 { \
 ASM(" PUSHW X \n" \
 " PUSH A \n" \
 " LDW X, #0xFFFF \n" \
 "loop1: LD A, #50 \n" \
 \
 "loop2: DEC A \n" \
 " JRNE loop2 \n" \
 \
 " DECW X \n" \
 " JRNE loop1 \n" \
 \
 " POP A \n" \
    " POPW X " );\
 }
/* not connected pins as output low state (the best EMC immunity)
(PA1, PA3, PA5, PB0, PB1, PB2, PB4, PC5, PC6, PD1, PD2, PD3, PD4, PD5,
 PD6, PD7)*/
#define CONFIG_UNUSED_PINS_STM8L001 \
{ \
 GPIOA->DDR |= GPIO_Pin_1 | GPIO_Pin_3 | GPIO_Pin_5; \
 GPIOB->DDR |= GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_4; \
 GPIOC->DDR |= GPIO_Pin_5 | GPIO_Pin_6; \
 GPIOD->DDR |= GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | \
 GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; \
}
/* pin for testing */
#define Led_PORT GPIOB
#define Led_PIN GPIO_Pin_6

main()
{
 uint16_t i;
 /* -------------STM8L001 startup-------------- */
 /* configure unbonded pins */
 CONFIG_UNUSED_PINS_STM8L001;
 /* delay for SWIM connection: ~5seconds */
 STARTUP_SWIM_DELAY_5S;
 /* ------------------------------------------- */

 /* configure all STM8L001 pins as input with pull up */
 GPIO_Init(GPIOA, GPIO_Pin_0, GPIO_Mode_In_PU_No_IT); // pin 1
 GPIO_Init(GPIOA, GPIO_Pin_2, GPIO_Mode_In_PU_No_IT); // pin 2
 GPIO_Init(GPIOD, GPIO_Pin_0, GPIO_Mode_In_PU_No_IT); // pin 5
 GPIO_Init(GPIOB, GPIO_Pin_6, GPIO_Mode_In_PU_No_IT); // pin 6
 GPIO_Init(GPIOB, GPIO_Pin_7, GPIO_Mode_In_PU_No_IT); // pin 7
 GPIO_Init(GPIOC, GPIO_Pin_2, GPIO_Mode_In_PU_No_IT); // pin 8

 /* initialize tested pin */
 GPIO_Init(Led_PORT, Led_PIN, GPIO_Mode_Out_PP_Low_Fast);

 while (1)

          {
            GPIO_ToggleBits(Led_PORT, Led_PIN);
            /* delay */
            for(i=0; i<64000; i++);
           } 
 }

Можно скопировать весь текст на странице «main.c». Главный цикл находится в самом конце, это несколько строчек. Все остальное — инициализация и подготовка. Переносим текст, нажимаем зеленый квадратик со стрелкой (ALT+F7) и компилятор должен выдать «Ошибок 0».

Переходим к прошивке. Используем ST-LINK: скачиваем и устанавливаем под него свежие драйвера. Вставляем в USB. Подключаем три провода программатора ST-LINK: Gnd, Swim, +3.3 к соответствующим выводам Модуля STM8L. Здесь нужно заметить, что на Модуле STM8L уже загружена тестовая прошивка. Поэтому, на все про все есть ровно 5 секунд. За это время нужно соединить три контакта и нажать кнопку «Download and Debug» (Ctrl+D). После прошивки рестарт, ждем 5 секунд и видим наш мигающий светодиод. Можно поменять в программе паузу, правда, чтобы её увеличить, необходимо задать переменную «i» как uint32.

Соединение

Соединение

Это первые шаги. В даташите приводится код на Си. Можно самостоятельно дописать нашу программу, чтобы можно было использовать UART и таймеры.

Введение

В 2016 году была написана серия статей «AVR на C — просто?» — Часть 1, Часть 2, Часть 3, Часть 4. Уже тогда были мнения что 8-битные микроконтроллеры ненужны. Но прошло 6 лет и 8-битные существуют наравне с 32-битными. Причем цена не играет существенной роли в выборе микроконтроллера. Целесообразно делать выбор в соответствии с поставленной задачей. Зачем использовать производительный и «нафаршированный» камень для простых задач, таких как поморгать светодиодом, сделать электронный градусник или простой вольтметр — показометр. Почему STM8? Почему не STM32? Почитав статью пришел к выводу что для начинающего любителя микроконтроллеров лезть в дебри 32-битной архитектуры на ядре ARM Cortex-M3 та еще задача. Пока разбирешся может пропасть интерес к микроконтроллерам. Да и для STM8 обучающие руководства далеки для понимания. Хочется чего то простого на подобии Ардуино, но написанного на напрямую, без «скетчей». И слово то какое придумали «скетч» созвучно со скотчем и на нем склеенное — это так отступление.

Для кого это все? Для начинающего программиста — любителя постараюсь изложить свой подход к изучению STM8. Профессионалам в области микроконтроллеров наверное будет не очень интересно, но конструктивная критика приветствуется, без навязывания своего видения процесса разработки простых устройств на микроконтроллере.

1. Подготовка

Нам понадобится:

  • Среда разработки (Code::Blocks);

  • Компилятор (SDCC — Small Device C Compiler);

  • Стандартная библиотека Standard Peripherals Library (SPL для STM8);

  • Программа для загрузки микропрограмм в микроконтроллер (ST Visual Programmer для STM8);

  • Программатор (обычный ST-Link подойдет);

  • Микроконтроллер (STM8S103F3P6 подойдет, лучше распаянный на макетной плате);

  • Прочее (провода, сопротивления, светодиоды, восьми сегментные индикаторы и др.).

1.1. Среда разработки

В качестве среды разработки остановимся на Code::Blocks. Скачать можно с сайта разработчика по ссылке. Скачивать можно последнюю версию, на сегодня это codeblocks-20.03. При этом предлагаю версию codeblocks-20.03mingw со встроенным компилятором GCC, вдруг вам захочется ознакомится c серией статей по AVR. Кстати описывать установку Code::Blocks в рамках данной статьи не вижу смысла, установка с подробностями и картинками описана в первой статье по AVR. Отмечу лишь что настройка для работы с STM8 будет подробно описана далее в этой статье.

1.2. Компилятор

Ну раз уж начали использовать свободно доступные программы, то и компилятор возьмем свободно распространяемый, а именно SDCC. К сожалению в GCC не поддерживается работа с STM8. Компилятор скачаем опять с сайта разработчиков по ссылке.

Рассмотрим установку под Win 7. Запускаем ранее скачанный файл sdcc-4.2.0-x64-setup.exe и по каритинкам:

  • Принимаем лицензионное соглашение.

  • Предлогает создать ссылку из меню «Пуск»

  • Список устанавливаемых компонентов

  • Путь установки

  • Установка

  • Добавление пути на файлы в PATH

  • И завершение установки

Весь процесс установки можно описать как запустить файл и Next – Iagree — Next — Next — Next — Next – Next – Finish.

1.3. Создание проекта

Для объединения SDCC и Code::Blocks добавим мастер создания проектов для STM8. Нам понадобится «мастер» создания проектов, скачаем скрипты по ссылке.

Из полученного архива codeblocks-wizard-stm8-master.zip извлекаем папку stm8. Ее нужно поместить по пути (в случае если у вас Win 64) для системы Win32 путь может отличатся.

C:\Program Files\CodeBlocks\share\CodeBlocks\templates\wizard

Теперь в этой же папке находим файл config.script и в функцию function RegisterWizards() добави строку

RegisterWizard(wizProject, _T(«stm8»), _T(«STM8 Project»), _T(«Embedded Systems»));

Должно получится примерно так

Теперь проверим работоспособность среды разработки для проектов STM8

Запускаем CodeBlocks и жмем Create a new project

Выбираем категорию проекта STM8 Project и жмем Go

Жмем Next>

Задаем название проекта (Project title:), каталог размещения проекта (Folder to crete project in:) и жмем Next>

Проверяем чтоб Compiler был Small Device C Compiler снимаем галку Create “Debug” configuration (хотя можна и оставить) и жмем Next>

Тут просто Finish.


Видим пустой проект (открыть main.c). Нажимаем вверху

Внижней части окна видим результат компиляции нашего, пока еще пустого проекта. Если в синем тексте нули то среда разработки готова для проектов STM8, но без использования Standard Peripherals Library (SPL для STM8)

1.4. Стандартная библиотека STM8

Чтоб облегчить разработку добавим модернизированные для работы с CDCC библиотеки SPL для STM8. Скачиваем доработанные библиотеки по ссылке.

Распаковываем архив STM8-SPL-SDCC-master.zip например

C:\STM8-SPL-SDCC-master

Подключаем к Code::Blocks

Менню Settings > Compiler

Подключаем библиотеки:

  • Selected compiler выбрать Small Device C Compiler

  • Вкладка Search directories

  • К имеющейся строке SDCC (выделена) добавляем строки библиотек SPL

1.4. Программа для загрузки микропрограмм в микроконтроллер

Для загрузки программ в микроконтроллер используем ST Visual Programmer (stvp) загрузить можно по ссылке на сайте STMicroelectronics. При первом запуске STVP запросит выбрать микроконтроллера

После выбора откроется программа

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

1.5. Программатор

В качестве программатора используем ST-Link v2 из известного магазина

1.6. Микроконтроллер

В качестве подопытного завалялась плата из поднебесной

Тут установлен микроконтроллер STM8S103F3P6, что подойдет для наших изысканий.

1.7. Прочее

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

Об этом в следующей части этой серии статей. Попробуем выяснить что программировать 8-битные микроконтроллеры от STM мало чем отличается от AVR.

Продолжение читайте в продолжении «STM8 просто на C. Часть 2 +»

Теги:


Опубликована:
Изменена: 28.09.2022

3


Вознаградить

Я собрал
0

0

x

Оценить статью

  • Техническая грамотность
  • Актуальность материала
  • Изложение материала
  • Полезность устройства
  • Повторяемость устройства
  • Орфография

0

Средний балл статьи: 0
Проголосовало: 0 чел.

(Ocr-Read Summary of Contents of some pages of the STMicroelectronics STM8 Document (Main Content), UPD: 25 July 2023)

  • 126, Epilogue In the end, I would like to share that my tiny raw-level knowledge and experiences with STM32s (http://embedded-lab.com/blog/stm32-tutorials/) earlier paid off handsomely. Due to that I was able to compiler this article decently and quickly. Personally, I feel that whosoever knows STM8 micros well will master STM32s and vice-versa because except the cores all hardware in both architectures are not jus…

  • 124, • Bitwise and logic operations are useful. Not only they are fast, they just deal with the designated bits only. SPL has support for such operations but it is still better to know them. Here are some common operations: #define bit_set(reg, bit_val) reg |= (1 << bit_val) //For setting a bit of a register #define bit_clr(reg, bit_val) reg &= (~(1 << bit_val)) //For clearing a bit of a register #define bit_tgl(reg, bit…

  • 104, Serial Peripheral Interface (SPI) SPI communication is an onboard synchronous communication method and is used by a number of devices including sensors, TFT displays, GPIO expanders, PWM controller ICs, memory chips, addon support devices, etc. There’s always one master device in a SPI communication bus which generates clock and select slave(s). Master sends commands to slave(s). Slave(s) responds to commands sent by the master. The number of slaves in a SPI b…

  • 55, STMicroelectronics STM8 The second line of the above function states that we are going to use ADC channel 0 (PB0) with no Schmitt trigger. We are also not going to use external triggers from timer/GPIO modules. Since the master clock is running at 8MHz, the ADC prescaler divides the master/peripheral clock to get a sampling frequency of 444kHz. We are also going to use continuous conversion mode because we want to continually read the ADC input and don’t…

  • 72, Time Base Generation (TIM2) Time base generation is the most basic property of any timer and is also the most needed requirement in embedded systems. This mode can be used with or without interrupt. We’ll first check the method firstly without interrupt and then with interrupt. With time base generation, we can accurately time stuffs and events that are more precise than using delays, loops or other methods. Time base gener…

  • 105, Hardware Connection Code Example main.c #include «STM8S.h» #include «MAX72XX.h» void clock_setup(void); void GPIO_setup(void); void SPI_setup(void); void main() { unsigned char i = 0x00; unsigned char j = 0x00; volatile unsigned char temp[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; const unsigned char text[96] = {

  • 64, Code Example #include «STM8S.h» void clock_setup(void); void GPIO_setup(void); void IWDG_setup(void); void main(void) { unsigned int t = 0; clock_setup(); GPIO_setup(); GPIO_WriteLow(GPIOD, GPIO_PIN_0); for(t = 0; t < 60000; t++); IWDG_setup(); while(TRUE) { GPIO_WriteReverse(GPIOD, GPIO_PIN_0); for(t = 0; t < 1000; t++) { if(GPIO_ReadInputPin(GPIOB, GPIO_PIN_7) == FALSE) …

  • 59, void ADC1_setup(void) { ADC1_DeInit(); ADC1_Init(ADC1_CONVERSIONMODE_SINGLE, ADC1_CHANNEL_1, ADC1_PRESSEL_FCPU_D10, ADC1_EXTTRIG_GPIO, DISABLE, ADC1_ALIGN_RIGHT, ADC1_SCHMITTTRIG_CHANNEL1, DISABLE); ADC1_AWDChannelConfig(ADC1_CHANNEL_1, ENABLE); ADC1_SetHighThreshold(600); ADC1_SetLowThreshold(200); ADC1_Cmd(ENABLE); } void …

  • 95, Explanation The clocks and peripherals are set first. We are using 2MHz peripheral clock and the CPU is running at 0.5MHz. CLK_HSIPrescalerConfig(CLK_PRESCALER_HSIDIV8); CLK_SYSCLKConfig(CLK_PRESCALER_CPUDIV2); …. …. CLK_PeripheralClockConfig(CLK_PERIPHERAL_TIMER1, ENABLE); CLK_PeripheralClockConfig(CLK_PERIPHERAL_TIMER2, ENABLE); GPIOs must be set too. Since TIM2 is to output PWM, its CH1 must be set …

  • 52, STMicroelectronics STM8 Hardware Connection Code Example #include «STM8S.h» void clock_setup(void); void GPIO_setup(void); void ADC1_setup(void); void lcd_print(unsigned char x_pos, unsigned char y_pos, unsigned int value); void main() { unsigned int A0 = 0x0000; clock_setup(); GPIO_setup(); ADC1_setup();

  • 108, void MAX72xx_init(void); void MAX72xx_write(unsigned char address, unsigned char value); MAX72xx.c #include «MAX72xx.h» void MAX72xx_init(void) { GPIO_Init(CS_port, CS_pin, GPIO_MODE_OUT_PP_HIGH_FAST); MAX72xx_write(shutdown_reg, run_cmd); MAX72xx_write(decode_mode_reg, 0x00); MAX72xx_write(scan_limit_reg, 0x07); MAX72xx_write(inten…

  • 119, STMicroelectronics STM8 Some Useful Tips When using a new compiler, I evaluate some certain things. For instance, how do I include my own written library files, interrupt management, what conventions I must follow and what dos and don’ts must be observed. Creation & Addition of libraries At some point in working with any microcontroller, you’ll need two basic libraries more than anything else. These are LCD and delay libraries. LCDs are great t…

  • 75, As explained earlier, to get 2 second timer reload interval we need to prescale the timer by 2048 and load its counter with 1952. This is what should be the setup for TIM2: void TIM2_setup(void) { TIM2_DeInit(); TIM2_TimeBaseInit(TIM2_PRESCALER_2048, 1952); TIM2_Cmd(ENABLE); } Our goal is to keep the LED on for 1 second and off for 1 second. It takes 1952 TIM2 counts for 2 second interval and so one second passes when this count is 976. Thus, in the main loop we are…

  • 23, Next, we set configuration bits if needed from the tab as shown below: Finally, we are ready to upload code. Just hit the start button and wait for the process to finish. Every time a code is programmed, it is verified automatically.

  • 19, During compilation, you may get tons of errors for hardware files that are not available in your target STM8S micro. For instance, CAN hardware is not available in STM8S003K3 and so if you have added CAN source and header files you will get an error for that. Once identified by the error messages, the corresponding header and source files for that particular hardware must be removed. Similarly, one more caution must be observed. Unless your code is using any …

This article will cover developing for STM8 series of microcontrolles completely from scratch, without using any vendor-supplied libraries.


Preface

STM8 is a cheap 8-bit microcontroller aimed towards low-cost mass-market devices. Initially I came across this part while searching for a simple microcontroller as a replacement for AVRs. Despite having various ARM Cortex-M0 devices available on the market for quite attractive prices, AVRs have one advantage — simplicity. Utilizing an ARM Cortex core to switch some lights on and off seems like an overkill. Some applications just don’t require that amount of flexibility and performance.

The main goal of this article is to demonstrate that ‘bare metal’ programming is not a difficult task and to give you an overview of STM8’s architecture and peripherals. Even though writing peripheral drivers from scratch might seem like reinventing the wheel, in many cases it is easier and faster to implement the functionality that you need for a specific task, instead of relying on vendor-supplied libraries that try to do everything at once (and fail).

Contents:

  • The Hardware
  • Setting up toolchain
  • It’s all just memory..
  • First program
  • Peripheral drivers
    – UART
    – SPI
    – I2C
    – ADC
    – Timers and interrupts
  • Putting it all together
  • Conclusion

The Hardware

There is a number of ways to start working with STM8. The easiest one is to get a Discovery board, although I wouldn’t recommend it, since STM8 Discovery boards aren’t that good and the on-board ST-Link v1 firmware just sucks.

Instead, I’ll opt for the minimalist approach. All you need is an ST-Link v2, STM8S003F3 and a breakout board. STM8S003F3 comes in a handy TSSOP20 package which is very easy to solder.

Poor man's devboard

Note: a 1uF capacitor on VCAP pin is required for the processor to operate.

The biggest downside is that STM8 processors are not supported by GCC. There are 3 commercial compilers available for these processors: Raisonance, Cosmic and IAR. Some of these compilers have free versions with code size limit, but none of them are available for linux. Luckily, SDCC supports STM8 and that’s what we’re going to use. SDCC is being actively developed, so I suggest trying the latest snapshot build instead of the stable version. To program the microcontroller we’ll be using stm8flash. The first step is to download all the necessary tools:

  1. sdcc
  2. stm8flash

Extract SDCC under ~/local/sdcc. Now extract stm8flash, build it with make and copy stm8flash binary to ~/local/sdcc/bin. I prefer to keep flasher with compiler for convenience. Next, add the following line to your .bashrc file (replacing username with your user name):

1
export PATH=$PATH:/home/username/local/sdcc

If everything was done properly, you should be able to run sdcc --version. The last remaining thing is to write udev rule for ST-Link programmer. Create a file /etc/udev/rules.d/99-stlink.rules:

1
2
3
# ST-Link v1/v2
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3744", MODE="0666"
ATTRS{idVendor}=="0483", ATTRS{idProduct}=="3748", MODE="0666"

Finally, run udevadm control --reload-rules && udevadm trigger as root. Now we’re all set and ready to start.

It’s all just memory..

Before we begin, let’s take a simple example of accessing port register on ATmega and see what’s going on under the hood:

1
2
3
4
5
6
7
8

PORTB = (1 << PB2);


(* (volatile uint8_t *) ((0x05) + 0x20)) = (1 << 2);


* (volatile uint8_t *) 0x25 = 0x04;

Typecasting integer to a pointer is a valid operation in C. If you don’t quite understand what is going on with pointer arithmetics then here’s another example for you:

1
2
3
uint8_t a = 0xDE;  
uint8_t *ptr = &a;
*ptr = 0xAD;

The only difference is that in the first example we know exactly which address in memory we are going to use. It’s important that you understand what’s going on here, since we’re going to use this mechanism for accessing hardware registers later on.

First program

These are the two most important documents: datasheet and reference manual. We’ll use the datasheet for the pinout and register map. Everything else is present in the reference manual: peripheral operation, register description, etc. Let’s begin by opening the GPIO section of the reference manual and taking a closer look at PORTD registers.

PORTD registers

These registers are pretty much self-explanatory but just in case, here’s a brief overview: DDR is the direction register, which configures a pin as either an input or an output. After we configured DDR we can use ODR for writing or IDR for reading pin state. Control registers CR1 and CR2 are used for configuring internal pull-ups, output speed and selecting between push-pull or pseudo open-drain.

First, let’s define a macro that we’ll use later on for register definitions. Base address for all the hardware registers is 0x5000 so we can hardcode that into our macro.

1
#define _SFR_(mem_addr)      (*(volatile uint8_t *)(0x5000 + (mem_addr)))

Now let’s try blinking an LED. For this task we need to define ODR, DDR and CR1 registers for PORTD. We also need a delay function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdint.h>

#define F_CPU 2000000UL

#define _SFR_(mem_addr) (*(volatile uint8_t *)(0x5000 + (mem_addr)))


#define PD_ODR _SFR_(0x0F)
#define PD_DDR _SFR_(0x11)
#define PD_CR1 _SFR_(0x12)

#define LED_PIN 4

static inline void delay_ms(uint16_t ms) {
uint32_t i;
for (i = 0; i < ((F_CPU / 18000UL) * ms); i++)
__asm__("nop");
}

void main() {
PD_DDR |= (1 << LED_PIN);
PD_CR1 |= (1 << LED_PIN);

while (1) {

PD_ODR ^= (1 << LED_PIN);
delay_ms(250);
}
}

Save this in main.c and compile by running the following command:

1
sdcc -lstm8 -mstm8 --out-fmt-ihx --std-sdcc11 main.c

Now attach st-link and flash the microcontroller.

1
stm8flash -c stlinkv2 -p stm8s003f3 -w main.ihx

It's alive!

Congratulations! We’ve just written our first program from scratch.

Note: some of the STM8 pins are labeled with (T) in the datasheet. These pins are ‘true’ open-drain and can only pull to ground. You should be extra careful when working with open-drain pins, since there are no protection diodes. I managed to accidentally blow PB5 by using it as a normal GPIO, which took me hours to figure out when my I2C code wasn’t working. One way of checking whether the pin is dead or not is by setting the multimeter in diode mode and measuring the voltage drop between the pin and ground — it should be roughly 0.7V in one direction.

Peripheral drivers

UART

After toggling some IO pins the first thing that you should get up and running on a new platform is UART. It makes debugging much easier. As always, we begin with register definitions.

1
2
3
4
5
6
7
8
9
10
11
12
13

#define UART_SR _SFR_(0x230)
#define UART_TXE 7
#define UART_TC 6
#define UART_RXNE 5

#define UART_DR _SFR_(0x231)
#define UART_BRR1 _SFR_(0x232)
#define UART_BRR2 _SFR_(0x233)
#define UART_CR1 _SFR_(0x234)
#define UART_CR2 _SFR_(0x235)
#define UART_TEN 3
#define UART_REN 2

Usually, in order to initialize UART one has to calculate baud and write the resulting value into the corresponding HIGH and LOW registers. Let’s see how this is done in STM8.

What were they thinking?!

So.. you get a 16-bit value and you write the first nibble [15:12] into BRR2[7:4], then you write bits [11:4] into BRR1 and finally you write the remaining bits [3:0] into BRR2[3:0]. Seriously, what were they thinking? Why couldn’t ST just implement BRR_HIGH and BRR_LOW for the sake of it? All this bit-fiddling just seems unnecessarily complicated.

Anyway, let’s move on to initialization. We’ll stick with the default 8 data bits, 1 stop bit and no parity. Since our master clock is 2MHz, for baud = 9600 we have UART_DIV = 2000000/9600 = 208 (0xD0). According to the bizarre diagram above, we end up with BRR1 = 0x0D and BRR2 = 0x00. One thing to keep in mind is that BRR2 register must be written before BRR1. Finally, we turn on receiver and transmitter in Control Register 2. Read and write functions are pretty straight-forward: you read/write the Data Register and wait until the appropriate bit in Status Register is set.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19




void uart_init() {
UART_BRR2 = 0x00;
UART_BRR1 = 0x0D;
UART_CR2 = (1 << UART_TEN) | (1 << UART_REN);
}

void uart_write(uint8_t data) {
UART_DR = data;
while (!(UART_SR & (1 << UART_TC)));
}

uint8_t uart_read() {
while (!(UART_SR & (1 << UART_RXNE)));
return UART_DR;
}

Redirecting stdout is easy with SDCC.

1
2
3
4
int putchar(int c) {
uart_write(c);
return 0;
}

Now we’re all set and we can use printf() for debugging.

SPI

Next, we implement SPI master. SPI is quite an easy peripheral and is usually implemented as a simple shift-register in hardware. We need to define only 4 registers to start working with SPI.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#define SPI_CR1 _SFR_(0x200)
#define SPE 6
#define BR0 3
#define MSTR 2
#define SPI_CR2 _SFR_(0x201)
#define SSM 1
#define SSI 0
#define SPI_SR _SFR_(0x203)
#define BSY 7
#define TXE 1
#define RXNE 0
#define SPI_DR _SFR_(0x204)


#define CS_PIN 4

Let’s implement initialization and read/write functions. Reading from SPI is achieved by writing a dummy byte, so we’ll hardcode SPI_write(0xFF) inside our SPI_read() function. Chip select pin will be managed in software.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36







void SPI_init() {

PC_DDR |= (1 << CS_PIN);
PC_CR1 |= (1 << CS_PIN);
PC_ODR |= (1 << CS_PIN);


SPI_CR2 = (1 << SSM) | (1 << SSI);
SPI_CR1 = (1 << MSTR) | (1 << SPE) | (1 << BR0);
}

void SPI_write(uint8_t data) {
SPI_DR = data;
while (!(SPI_SR & (1 << TXE)));
}

uint8_t SPI_read() {
SPI_write(0xFF);
while (!(SPI_SR & (1 << RXNE)));
return SPI_DR;
}

void chip_select() {
PC_ODR &= ~(1 << CS_PIN);
}

void chip_deselect() {
PC_ODR |= (1 << CS_PIN);
}

To test our implementation I’ve written a simple loop that transmits some data.

1
2
3
4
5
6
7
8
9
void main() {
SPI_init();
while (1) {
chip_select();
for (uint8_t i = 0xAA; i < 0xFA; i += 0x10)
SPI_write(i);
chip_deselect();
}
}

Let’s hook up the logic analyzer and have a look.

SPI transmission

Hmm.. something is wrong. It seems that we release chip select too early and the last byte will not be received by a slave device. This can only occur if the SPI peripheral didn’t have enough time to finish transmitting before we released CS pin.

That wasn’t supposed to happen — we are polling for TXE bit, aren’t we? Well, the problem is that TXE only indicates that Tx buffer is empty. It doesn’t tell us that all the bits were shifted out by the shift register. So in order to properly end the transmission we have to check for BSY flag, which tells us whether or not SPI has finished an operation. Let’s modify our chip_deselect() function to take that into account.

1
2
3
4
void chip_deselect() {
while ((SPI_SR & (1 << BSY)));
PC_ODR |= (1 << CS_PIN);
}

Final output.

SPI fixed

Our final test is the good old “Nokia 5110” LCD. Complete source is on github.

Nokia LCD

I2C

Now let’s get onto something more serious. I2C usually requires a bit more work to get it up and running comparing to SPI and UART. I2C has a lot of associated registers, so I will no longer list them from this point. You can find a header with register definitions here.

Let’s take a look at what the reference manual says about receive and transmit operations.

I2C transmit and receive modes

That does seem quite complicated: a lot of events are generated during communication. However, we don’t have to explicitly take care of every single event in order to have a working communication — some of the events are automatically cleared by hardware and some may just be ignored and left unattended. We’ll go with the easiest implementation.

We start by implementing initialization and IO functions. We also need dedicated functions to generate start and stop conditions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35





void i2c_init() {
I2C_FREQR = (1 << I2C_FREQR_FREQ1);
I2C_CCRL = 0x0A;
I2C_OARH = (1 << I2C_OARH_ADDMODE);
I2C_CR1 = (1 << I2C_CR1_PE);
}

void i2c_start() {
I2C_CR2 |= (1 << I2C_CR2_START);
while (!(I2C_SR1 & (1 << I2C_SR1_SB)));
}

void i2c_stop() {
I2C_CR2 |= (1 << I2C_CR2_STOP);
while (I2C_SR3 & (1 << I2C_SR3_MSL));
}

void i2c_write(uint8_t data) {
I2C_DR = data;
while (!(I2C_SR1 & (1 << I2C_SR1_TXE)));
}

uint8_t i2c_read(uint8_t ack) {
if (ack)
I2C_CR2 |= (1 << I2C_CR2_ACK);
else
I2C_CR2 &= ~(1 << I2C_CR2_ACK);
while (!(I2C_SR1 & (1 << I2C_SR1_RXNE)));
return I2C_DR;
}

According to the reference manual, writing slave address is a special case so we can’t simply use i2c_write() to do that. We need a dedicated function for this purpose.

1
2
3
4
5
6
void i2c_write_addr(uint8_t addr) {
I2C_DR = addr;
while (!(I2C_SR1 & (1 << I2C_SR1_ADDR)));
(void) I2C_SR3;
I2C_CR2 |= (1 << I2C_CR2_ACK);
}

Reference manual says we are supposed to to handle EV6 event after writing slave address: “EV6: ADDR=1, cleared by reading SR1 register followed by reading SR3”. After polling for ADDR bit we simply read SR3 register. I’m not sure why this is required, probably to check for BUS_BUSY, but that seemed a bit pointless so we cheated a little.

Now, let’s test our library with an HMC5883L magnetometer. First we define R/W flags and some magnetometer related stuff:

1
2
3
4
5
6
7
8
9
#define I2C_READ            0x01
#define I2C_WRITE 0x00

#define HMC5883_ADDR (0x1E << 1)
#define HMC5883_CR_A 0x00
#define HMC5883_CR_B 0x01
#define HMC5883_MODE 0x02
#define HMC5883_DATA_OUT 0x03
#define HMC5883_ID_REG_A 0x0A

We’ll implement a simple function that reads the device Id and sends it over UART.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void hmc5883_get_id(uint8_t *id) {

i2c_start();
i2c_write_addr(HMC5883_ADDR + I2C_WRITE);
i2c_write(HMC5883_ID_REG_A);
i2c_stop();


i2c_start();
i2c_write_addr(HMC5883_ADDR + I2C_READ);
id[0] = i2c_read(1);
id[1] = i2c_read(1);
id[2] = i2c_read(0);
i2c_stop();
}

int main() {
uint8_t id[3];
uart_init();
i2c_init();

while (1) {
hmc5883_get_id(id);
printf("Device ID: %c%c%c\n", id[0], id[1], id[2]);
delay_ms(250);
}
}

Output:

1
Device ID: H43

All seems to work fine, but let’s take a look at the logic analyzer just to make sure.

I2C receiver (broken)

Hmm.. we do receive correct bytes, but what’s the deal with that 0xFF received right after the NACK? It seems that something is wrong with our code. Time to RTFM.

The Proper Way

So the first problem is how we generate STOP condition. According to the documentation, we are supposed to generate STOP before reading the last byte. I changed the code but it didn’t fix the problem. The real problem was that I was porting the magnetometer driver which I wrote for a different microcontroller, so I expected the I2C peripheral to work in a certain way. Well, I was wrong.

The i2c_read() function is supposed to receive only 1 byte of data. It turns out there are 3 different scenarios for N=1, N=2 and N>2, where N is the number of received bytes. We can’t simply use the function for N=1 to read more than a single byte. That means we need separate functions to handle each case! I wonder how many logic gates were dedicated to implement I2C peripheral on this MCU… (Note: I2C implementation on STM32F1xx series is actually identical to STM8.)

Looking at the reference manual I figured that we could possibly combine N=2 and N>2 cases and handle them with a single function. Below are proper implementations of I2C receive functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint8_t i2c_read() {
I2C_CR2 &= ~(1 << I2C_CR2_ACK);
i2c_stop();
while (!(I2C_SR1 & (1 << I2C_SR1_RXNE)));
return I2C_DR;
}

void i2c_read_buf(uint8_t *buf, int len) {
while (len-- > 1) {
I2C_CR2 |= (1 << I2C_CR2_ACK);
while (!(I2C_SR1 & (1 << I2C_SR1_RXNE)));
*(buf++) = I2C_DR;
}
*buf = i2c_read();
}

Now let’s update our code for reading device Id.

1
2
3
4
5
6
7
8
9
10
11
12
void hmc5883_get_id(uint8_t *id) {

i2c_start();
i2c_write_addr(HMC5883_ADDR + I2C_WRITE);
i2c_write(HMC5883_ID_REG_A);
i2c_stop();


i2c_start();
i2c_write_addr(HMC5883_ADDR + I2C_READ);
i2c_read_buf(id, 3);
}

Note that our i2c_read_buf() function generates STOP so we no longer have to call i2c_stop() manually. Let’s take a look at the logic analyzer now.

I2C fixed

Great, no 0xFF at the end! Now we’re ready to move onto something different.

ADC

Nothing exciting about the ADC on STM8: 10-bit resolution, single and continuous conversion modes, configurable prescaler.. all the usual boring stuff. There is also a data buffer that can hold a number of ADC samples, which is rather convenient.

The default printf() implementation provided by SDCC does not support floats. To enable floating point output, printf_large.c needs to be recompiled with -DUSE_FLOATS=1 option. For this example we are going to cheat and print the results in millivolts instead. Without further ado, let’s write some code for single ADC conversion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define V_REF 3.3

uint16_t ADC_read() {
uint8_t adcH, adcL;
ADC1_CR1 |= (1 << ADC1_CR1_ADON);
while (!(ADC1_CSR & (1 << ADC1_CSR_EOC)));
adcL = ADC1_DRL;
adcH = ADC1_DRH;
ADC1_CSR &= ~(1 << ADC1_CSR_EOC);
return (adcL | (adcH << 8));
}

void ADC_init() {

ADC1_CSR |= (1 << 2);

ADC1_CR2 |= (1 << ADC1_CR2_ALIGN);

ADC1_CR1 |= 1 << ADC1_CR1_ADON;
}

void main() {
ADC_init();
uart_init();

while (1) {
uint16_t val = ADC_read();
float voltage = (V_REF / 1024.0) * val * 1000;
printf("Channel4: %d mV\n", (uint16_t) voltage);
delay_ms(250);
}
}

Pretty straight forward. Note that EOC flag has to be manually cleared by software.

A few things that should be taken into account when working with ADC:

  • The order in which DRL and DRH registers are accessed depends on data alignment.
  • ADC has no internal voltage reference. STM8S003 does not have an external Vref pin, so it is tied to Vcc internally, which means that your supply voltage has to be spot-on for any serious measurements.
  • Data buffer registers have no internal locking. ST provides an assembly snippet in the datasheet for reading buffer registers.

Timers and interrupts

You can’t get far without using timers and interrupts, which is what this last section will cover. STM8S003 has 16-bit ‘advanced control’ as well as 8-bit general-purpose timers. TIM1 is a really complicated peripheral with 32 dedicated registers, and covering it’s functionality would probably require a few extra articles. For this article, we’ll use TIM4 which is good enough for basic applications.

There isn’t much to tweak inside TIM4: it contains an 8-bit auto-reload up counter, 3-bit prescaler and an option to generate interrupt on counter overflow.

The prescaler divides counter clock frequency by a power of 2 from 1 to 128 depending on PSCR registers:

In this example we are going to toggle a pin each time the counter matches value in the ARR register. The frequency of the waveform generated by our IO pin is calculated as follows:

To achieve a frequency of 100Hz ARR has to be set to 77, given that our clock frequency is 2MHz. We need to enable Update Interrupt for TIM4, but before that interrupts must be enabled globally by executing rim instruction.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main() {

__asm__("rim");


PD_DDR |= (1 << OUTPUT_PIN);
PD_CR1 |= (1 << OUTPUT_PIN);


TIM4_PSCR = 0b00000111;


TIM4_ARR = 77;

TIM4_IER |= (1 << TIM4_IER_UIE);
TIM4_CR1 |= (1 << TIM4_CR1_CEN);

while (1) {

}
}

Now, when I said that we’re going to implement everything from scratch, I wasn’t completely honest. We’re still using some start-up code which initializes the stack and interrupt vector table. If you look at the listing you can see that SDCC has generated the interrupt table for us:

1
2
3
4
5
000000 82v00u00u00             37         int s_GSINIT ;reset
000004 82 00 00 00 38 int 0x0000 ;trap
000008 82 00 00 00 39 int 0x0000 ;int0
...
00007C 82 00 00 00 68 int 0x0000 ;int29

Registering an interrupt handler is easy with SDCC: there is a special attribute _interrupt() which takes interrupt number as a parameter. Section 7 (‘Interrupt vector mapping’) of the datasheet describes which IRQ number corresponds to which peripheral. For TIM4 it is 23. Our interrupt handler will look like this:

1
2
3
4
5
6
#define TIM4_ISR 23

void timer_isr(void) __interrupt(TIM4_ISR) {
PD_ODR ^= (1 << OUTPUT_PIN);
TIM4_SR &= ~(1 << UIF);
}

Putting it all together

We have enough building blocks — now it’s time to put them together into some ‘real-world’ application. For this demo I picked up MMA8452 3-axis I2C accelerometer and a standard HD44780 1602 LCD, which is extremely popular among electronics enthusiasts for some reason.

The demo application will calculate inclination angle based on accelerometer readings and output it to the LCD. Calculating inclination angle will require some trigonometry and floating point arithmetic, which will consume a good amount of resources. Despite the floating point operations being quite slow, STM8 managed this task decently.

Demo

You might have noticed the lack of contrast adjustment potentiometer. The LCD module that I’m using is rated for 5V, however my setup uses 3.3V supply. I couldn’t be bothered with a separate supply for the display, so I cheated: the LCD is initialized in 1-line mode, which results in 1/8 duty cycle, and Vo pin is tied to ground.

Conclusion

STM8 is nice and cheap, but it is really hard to justify using this microcontroller, especially given the fact that price difference between STM8 and low-end Cortex-M0 devices like STM32F03 is negligible. The biggest downside for me was lack of GCC support. Despite SDCC being a reasonably good compiler, it does not fully support C99 and C11 standards, which means that I have to refactor most of my existing code to make it compatible. Code optimization isn’t great either, which is a shame, since most STM8 microcontrollers don’t have a lot of flash to spare.

As always, code is available on github.

Понравилась статья? Поделить с друзьями:
  • Фуразолидон инструкция по применению для детей 4 года дозировка
  • Как собрать из лего грузовик инструкция по сборке
  • Основное содержание военной реформы под руководством военного министра милютина 1860 1870
  • Руководства по спасанию на судне
  • Амоксициллин клавулановая кислота 875мг 125мг инструкция как принимать взрослым