2в. STM8: восьмибитный родственник — что взять, а что нет
Разделы 2–2б — о 32-битных STM32. У ST есть и восьмибитное семейство — STM8: STM8S для обычных задач и STM8L для батарейных. Периферию у него и у STM32 делали из одних блоков, поэтому STM8 выглядит знакомо, но ядро другое — восьмибитное, со своими командами. Здесь — чем STM8 похож на STM32, чем нет, и что это значит в коде: одно и то же мигание и одна и та же арифметика собраны для обоих ядер и сравнены по листингам.
Опоры: даташит STM8S105 (DocID14771), заметка ST AN2945 о переходе между
STM8S и STM32, заголовок библиотеки ST для STM8L15x (stm8l15x.h, V1.5.0),
файлы описания STM8 в OpenOCD, даташит STM32F103 (DocID13587) из пака Keil.
STM8 собран компилятором SDCC 4.2 в Docker-образе stm-sdcc:1, F103 —
armclang. Симулятора STM8 в образе нет, платы тоже: всё сверено
по листингам и документам.
Как это называется в документации
Оригинал Перевод accumulator (A) аккумулятор: восьмибитный регистр для арифметики index registers X, Y индексные регистры по 16 бит condition code register (CC) регистр флагов SWIM — single wire interface module однопроводной интерфейс отладки и прошивки ICP / IAP прошивка через SWIM / из своей программы option bytes байты настроек (защита, загрузчик, переназначение выводов) data EEPROM EEPROM для данных внутри кристалла ROP — read-out protection защита от чтения
2в.1 STM8 рядом с STM32
AN2945 сравнивает ядро STM8 с Cortex-M3 в первом поколении STM32 (таблица 1):
| STM8 | Cortex-M3 | |
|---|---|---|
| Ширина данных | 8 бит | 32 бита |
| Dhrystone, DMIPS, без тактов ожидания | 0,29 | 1,22 |
| Команды | CISC | RISC |
| Средняя длина команды | 2 байта | 2 байта |
| Задержка входа в прерывание | 9 тактов | 12 тактов |
| Отладка | один провод (SWIM) | два провода или JTAG |
| Частота | до 24 МГц у STM8S | до 72 МГц |
По Dhrystone STM8 в 4,2 раза слабее, предельная частота — втрое ниже. Зато код такой же плотный, а прерывание начинается на три такта раньше. Периферия общая по устройству: в AN2945 регистры SPI у STM8S и STM32 почти совпадают по именам и битам, отличаются ширина регистров, DMA и частности вроде портов. Кто знает таймеры и SPI у STM32 (разделы 13 и 18), быстро разберётся и в STM8.
2в.2 Ядро: A, X, Y и 24-битный счётчик команд
По даташиту STM8S105 (п. 4.1) у ядра шесть регистров:
- A — восьмибитный аккумулятор, через него идёт арифметика;
- X и Y — индексные регистры по 16 бит, они же пара для 16-битных чисел;
- SP — указатель стека, 16 бит;
- PC — счётчик команд, 24 бита: адресное пространство 16 МБ;
- CC — флаги, 8 бит, из них 7 флагов: 5 флагов результата и 2 бита маски прерываний.
80 команд, 20 способов адресации, 8 × 8 умножение и деление 16 на 8 и 16 на 16 бит. Для сравнения: у Cortex-M3 шестнадцать 32-битных регистров (раздел 1.3), а умножение у F103, по его даташиту, занимает один такт.
В листинге SDCC видно, как компилятор раскладывает числа по этим регистрам. 16-битная сумма: первый аргумент приходит в X, второй — в стеке:
_add16:
000000 72 FB 03 [2] addw x, (0x03, sp)
000003 90 85 [2] popw y
000005 5B 02 [2] addw sp, #2
000007 90 FC [2] jp (y)
Результат остаётся в X. Три последние команды — возврат: функция сама
снимает свой аргумент со стека (достаёт адрес возврата в Y, сдвигает SP
и прыгает по Y). В скобках — такты команды, их печатает ассемблер SDCC.
32-битная сумма лежит в паре Y:X — младшее слово в X, старшее в Y;
перенос между ними — условный переход jrnc и команда incw y.
У SDCC для STM8 int — 2 байта, long — 4, указатель — 2 байта; это
подтверждает _Static_assert в опыте. Как у AVR (раздел 80): выражение
1000 * 60 в int уже переполняется.
2в.3 Память и такт после сброса
Карта памяти STM8S105 (даташит, рисунок карты и таблица 7):
| Адреса | Что там |
|---|---|
| 0x0000–0x07FF | ОЗУ, 2 КБ; вершина стека — конец ОЗУ |
| 0x4000–0x43FF | EEPROM для данных, 1 КБ |
| 0x4800–0x487F | байты настроек |
| 0x5000–0x57FF | порты и периферия |
| 0x6000–0x67FF | загрузочное ПЗУ, 2 КБ |
| 0x7F00… | регистры ядра, SWIM, отладки и прерываний |
| 0x8000–0x807F | 32 вектора прерываний |
| 0x8000–0xFFFF | Flash программы, до 32 КБ |
Регистры ядра видны отладчику по адресам: A — 0x7F00, SP — 0x7F08 и 0x7F09. После сброса SP = 0x07FF — вершина ОЗУ, поэтому стартовому коду не нужно ставить стек. EEPROM выдерживает 300 тыс. циклов записи (даташит) — у F103 отдельной EEPROM нет, в описании пака у F103C8 только Flash и ОЗУ.
Другие камни — по файлам на диске:
| Flash | EEPROM | Источник | |
|---|---|---|---|
| STM8S103 | 0x8000–0x9FFF, 8 КБ | 0x4000–0x427F, 640 байт | конфигурация OpenOCD |
| STM8L15x малой ёмкости | 0x8000–0x9FFF, 8 КБ | 0x1000–0x10FF, 256 байт | stm8l15x_flash.h |
| STM8L15x средней ёмкости | 0x8000–0xFFFF, 32 КБ | 0x1000–0x13FF, 1 КБ | stm8l15x_flash.h |
| STM8L15x высокой ёмкости | 0x8000–0x17FFF, 64 КБ | 0x1000–0x17FF, 2 КБ | stm8l15x_flash.h |
У старших STM8L Flash выходит за 0xFFFF — вот зачем 24-битный счётчик команд. Указатель в SDCC при этом 2 байта: данные за 64 КБ так не достать.
Такт после сброса у STM8S и STM8L одинаковый — HSI 16 МГц, делённый на 8, то есть 2 МГц, — но регистр делителя в разных местах:
| STM8S105 (даташит) | STM8L15x (stm8l15x.h) |
|
|---|---|---|
CLK_CKDIVR |
0x50C6, после сброса 0x18 | 0x50C0, после сброса 0x03 |
Такты периферии CLK_PCKENR1 после сброса |
0xFF — всё включено | 0x00 — всё выключено |
| Бит такта для портов | нет | нет |
| Медленный внутренний генератор | 128 кГц | 38 кГц |
Порты у обоих без отдельного такта: включать нечего, в отличие от STM32 (раздел 8.4). Адреса портов совпадают — порт B с 0x5005 и там и там.
2в.4 Мигание по регистрам: SDCC против armclang
Библиотека ST для STM8 к SDCC не подходит: stm8l15x.h знает только
компиляторы Cosmic, Raisonance и IAR, а на остальных останавливает сборку
строкой #error "Unsupported Compiler!". Поэтому адреса — свои, из
даташита STM8S105 (таблицы 8 и 9). Светодиод — на PB5:
#define PB_ODR (*(volatile uint8_t *)0x5005)
#define PB_DDR (*(volatile uint8_t *)0x5007)
#define PB_CR1 (*(volatile uint8_t *)0x5008)
#define PB_CR2 (*(volatile uint8_t *)0x5009)
#define CLK_CKDIVR (*(volatile uint8_t *)0x50C6)
#define LED (1u << 5) /* светодиод на PB5 */
void clock_16mhz(void)
{
CLK_CKDIVR = 0x00; /* после сброса 0x18: HSI / 8 = 2 МГц */
}
void led_init(void)
{
PB_DDR |= LED; /* выход */
PB_CR1 |= LED; /* двухтактный */
PB_CR2 &= (uint8_t)~LED; /* медленный фронт */
}
void led_toggle(void)
{
PB_ODR ^= LED;
}
Сборка — sdcc -mstm8 --std-c11 blink_stm8.c, без предупреждений.
Листинг .rst после компоновки:
_led_init:
008029 72 1A 50 07 [1] bset 0x5007, #5
00802D 72 1A 50 08 [1] bset 0x5008, #5
008031 72 1B 50 09 [1] bres 0x5009, #5
008035 81 [4] ret
_led_toggle:
008036 90 1A 50 05 [1] bcpl 0x5005, #5
00803A 81 [4] ret
Каждое |=, &= ~ и ^= стало одной командой над битом прямо в памяти:
bset, bres, bcpl. Прерывание не может вклиниться между чтением и
записью — это одна команда. У F103 для того же нужен BSRR или
bit-banding (разделы 8.2 и 1.5). Тот же led_toggle у armclang:
led_toggle:
f640400c MOV r0,#0xc0c
f2c40001 MOVT r0,#0x4001
6801 LDR r1,[r0,#0]
f0810120 EOR r1,r1,#0x20
6001 STR r1,[r0,#0]
4770 BX lr
Два 32-битных слова уходят только на то, чтобы собрать адрес 0x4001 0C0C
(GPIOB->ODR), дальше — чтение, «исключающее или», запись. Итог по
функциям и образу целиком:
| STM8S, SDCC | STM32F103, armclang -Os | |
|---|---|---|
led_init |
4 команды, 13 байт | 12 команд, 36 байт |
led_toggle |
2 команды, 5 байт | 6 команд, 18 байт |
| Образ мигания | 89 байт | 456 байт |
| Из них векторы | 4 байта (только сброс) | 236 байт (59 слов) |
Образ STM8: вектор сброса и переход на main — 7 байт, обнуление и
копирование данных — 26, переход к запуску — 3, код — 53. Вектор сброса — 82 00 80 07:
команда int и 24-битный адрес. SDCC пишет в таблицу только используемые
векторы. Образ F103 собран со стартовым файлом пака Keil и microlib; в нём
236 байт таблицы векторов. На 8 КБ STM8S103 мигание — это 89 байт,
на 64 КБ F103 — 456: разница заметна только на самых маленьких камнях.
2в.5 Арифметика по листингам
Один исходник arith.c собран обоими компиляторами. Модель на ПК
(stm8_2c.py) считает по листингам команды и байты, а у STM8 — и сумму
тактов из квадратных скобок. У fromelf тактов в листинге нет. Всё вместе
с возвратом из функции; где STM8 зовёт библиотеку, её подпрограмма собрана
из исходников SDCC и прибавлена:
| Операция | STM8: команд | байт | тактов | Cortex-M3: команд | байт |
|---|---|---|---|---|---|
| 16 + 16 бит | 4 | 9 | 8 | 3 | 6 |
| 32 + 32 бита | 7 | 15 | 14 | 2 | 4 |
| 8 × 8 бит | 6 | 9 | 11 | 2 | 4 |
8 × 8 с приведением к uint16_t |
32 | 51 | 56 | 2 | 4 |
| 16 × 16 бит | 25 | 42 | 50 | 3 | 6 |
| 32 × 32 бита | 86 | 142 | 149 | 2 | 4 |
- Сложение. 16 бит — родная ширина пары X, одна команда
addw. 32 бита — два сложения и перенос. - 8 × 8 — одна команда
mul x, a(4 такта по листингу). - 16 × 16 — вызов
__mulint: 19 команд, внутри триmul x, a. - 32 × 32 — вызов
__mullong: 75 команд, 124 байта, десятьmul x, a, 149 тактов вместе с подготовкой аргументов. У Cortex-M3 — однаMULS. - Приведение до умножения.
(uint16_t)a * bвместоa * bделает из 8 × 8 умножение 16 × 16: 6 команд превращаются в 32 с вызовом__mulint, хотя результат тот же.
Вывод: на STM8 держите данные в 8 и 16 битах. Каждый uint32_t и тем
более float — вызов библиотеки.
2в.6 Отладка, прошивка и когда STM8 уместен
SWIM. Один провод данных вместо двух у SWD: у STM8S105 это вывод PD1.
По даташиту SWIM работает во всех режимах камня, передаёт до 145 байт
в миллисекунду; модуль отладки даёт программные точки останова на любой
команде и две сложные. Протокол описан в UM0470. Программатор — ST-LINK:
в OpenOCD STM8 подключается только через SWIM (transport select swim),
а сетевой ST-Link-сервер SWIM не поддерживает (руководство OpenOCD).
Для STM8S103 в OpenOCD готов файл: Flash 8 КБ, блок записи 64 байта.
Защита и загрузчик. Защита чтения включается байтом ROP = 0xAA и
меняется только через SWIM; любая попытка её снять стирает Flash и EEPROM.
Вывода BOOT0, как у STM32 (раздел 4.9), нет: загрузочное ПЗУ после сброса
смотрит байты 0x487E, 0x487F и вектор сброса и само решает, идти ли
в загрузчик.
Среда. ST даёт STVD и программатор STVP; в даташите 2010 года бесплатные Cosmic и Raisonance ограничены 16 КБ кода. SDCC — без ограничений, но без библиотеки ST: регистры описываете сами, как в 2в.4. CubeMX, HAL и LL для STM8 нет — всё это только у STM32.
Когда STM8 уместен:
- питание 5 В: STM8S105 работает от 2,95 до 5,5 В, F103 — от 2,0 до 3,6 В;
- нужна настоящая EEPROM на 300 тыс. циклов, а не запись во Flash;
- задача мала: опрос кнопок, реле, простой ШИМ, UART — хватает 8 КБ Flash STM8S103 или 32 КБ и 2 КБ ОЗУ STM8S105, а камень самый простой;
- продолжение старого изделия на STM8: AN2945 прямо предлагает переход в обе стороны.
Когда нет:
- 32-битная арифметика, фильтры, плавающая точка — см. 2в.5;
- нужны USB, Ethernet, много ОЗУ, RTOS;
- новый проект без ограничений по питанию: у STM32 одна среда, HAL и примеры на всё семейство, у STM8 — старая библиотека 2011 года и компиляторы по отдельности.
Цен на диске нет, поэтому сравнения по цене здесь нет: её смотрят у поставщика на момент заказа (раздел 2.5).
2в.7 Грабли
Код с STM8S не заводит такт на STM8L. Адреса портов общие, а CLK_CKDIVR
у S — 0x50C6, у L — 0x50C0. Запись по чужому адресу попадает в другой
регистр.
На STM8L молчит таймер или UART. После сброса CLK_PCKENR1 = 0x00 —
такт периферии выключен, в отличие от STM8S (0xFF). Порты при этом работают:
у них отдельного такта нет.
Библиотека ST не собирается в SDCC. stm8l15x.h понимает только
Cosmic, Raisonance и IAR: #error "Unsupported Compiler!". Для SDCC —
свои определения регистров из даташита.
Лишнее приведение типа. (uint16_t)a * b для двух uint8_t — вызов
__mulint вместо одной команды mul: 32 команды вместо 6.
int на 16 бит. 1000 * 60 переполняется, счётчики миллисекунд
в int заканчиваются на 32 767. Берите uint32_t там, где это правда
нужно, и помните, что это вызов библиотеки при умножении.
Защита чтения стирает всё. Снять ROP без стирания Flash и EEPROM
нельзя — данные в EEPROM тоже пропадут.
2в.8 Итоги
- STM8 — восьмибитное CISC-ядро: A 8 бит, X и Y по 16, PC 24 бита; по AN2945 — 0,29 против 1,22 по Dhrystone и 9 тактов до прерывания против 12 у Cortex-M3.
- STM8S105: до 16 МГц, 32 КБ Flash, 2 КБ ОЗУ, 1 КБ EEPROM, питание 2,95–5,5 В; после сброса 2 МГц (HSI / 8), SP = 0x07FF.
- Порты у STM8S и STM8L по одним адресам и без такта; делитель такта и включение периферии — разные.
- Мигание:
bset,bres,bcpl— одна команда на бит;led_toggle2 команды и 5 байт против 6 и 18 у F103; образ 89 байт против 456. - Арифметика: 16-битное сложение — 4 команды, 32 × 32 — 86 команд
и 149 тактов с
__mullongпротив однойMULS. - Отладка — SWIM по одному проводу; загрузчик выбирают байтами настроек, а не выводом.
Полные исходники
blink_stm8.c — мигание светодиодом на STM8S по регистрам, сборка SDCC.
/* Раздел 2в: мигание светодиодом на STM8S по регистрам, компилятор SDCC.
Адреса — даташит STM8S105 (DocID14771): таблица 8 (порты) и таблица 9 (CLK). */
#include <stdint.h>
_Static_assert(sizeof(int) == 2 && sizeof(long) == 4 && sizeof(void *) == 2,
"SDCC для STM8: int 2 байта, long 4, указатель 2");
#define PB_ODR (*(volatile uint8_t *)0x5005)
#define PB_DDR (*(volatile uint8_t *)0x5007)
#define PB_CR1 (*(volatile uint8_t *)0x5008)
#define PB_CR2 (*(volatile uint8_t *)0x5009)
#define CLK_CKDIVR (*(volatile uint8_t *)0x50C6)
#define LED (1u << 5) /* светодиод на PB5 */
void clock_16mhz(void)
{
CLK_CKDIVR = 0x00; /* после сброса 0x18: HSI / 8 = 2 МГц */
}
void led_init(void)
{
PB_DDR |= LED; /* выход */
PB_CR1 |= LED; /* двухтактный */
PB_CR2 &= (uint8_t)~LED; /* медленный фронт */
}
void led_toggle(void)
{
PB_ODR ^= LED;
}
static void delay(uint16_t n)
{
while (n--) {
__asm__("nop");
}
}
void main(void)
{
clock_16mhz();
led_init();
for (;;) {
led_toggle();
delay(50000u);
}
}
blink_f103.c — то же мигание на STM32F103 через CMSIS, сборка armclang.
/* Раздел 2в: то же мигание на STM32F103 по регистрам CMSIS, компилятор armclang. */
#include "stm32f10x.h"
#define LED (1u << 5) /* светодиод на PB5 */
void led_init(void)
{
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; /* у STM32 порту нужен такт */
GPIOB->CRL = (GPIOB->CRL & ~(0xFu << 20)) | (0x2u << 20); /* PB5: выход 2 МГц, двухтактный */
}
void led_toggle(void)
{
GPIOB->ODR ^= LED;
}
static void delay(uint32_t n)
{
while (n--) {
__asm volatile("nop");
}
}
int main(void)
{
led_init();
for (;;) {
led_toggle();
delay(50000u);
}
}
Дальше: раздел 2г «Миландр К1986ВЕ92: отечественный Cortex-M3 рядом с STM32» — чем отечественный камень на знакомом ядре отличается от F103 и что переносится без правок.