12 канальный ШИМ на STM8. А также альтернативная библиотека и make в linux

В среде профессионалов, которые измеряют партии своих устройств в килоюнитах и считают контроллеры, меньше чем на 100 выводов ерундой, часто всплывает одна парадоксальная тема. А какой контроллер сейчас стоит рублей 20–30 и подойдет для бомж DIY? AVR после продажи компании ATMEL подорожали, STM8 после кризиса полупроводников подорожали тоже, но не так сильно. Я купил свои 15 портовые STM8S003 по 25 рублей за штуку. Конечно, CH32V003 дышат им в спину, но о них позже.

Тулкит

Компилятор SDCC есть в репозитории debian, прошивальщик есть на гитхабе. Для установки в debian:

apt install sdcc pkg-config libusb-1.0-0-dev
git clone https://github.com/vdudouyt/stm8flash
cd stm8flash
make

Теперь есть чем прошить и скомпилировать.

makefile

Странно, но я не нашёл адекватного примера мейкфайла на гитхабе, пришлось писать свой. Здесь вcё как и в адекватных мейкфайлах, добавляем исходный код в переменную SOURCES, директории с заголовочниками в переменную INCLUDES, а название проекта в TARGET, бинарник забирать в hex/$(TARGET).ihx. Код:

Скрытый текст

#main information
CC = sdcc
CFLAGS = --std-c99 -mstm8 --opt-code-size --allow-unsafe-read
RELS = $(SOURCES:%.c=$(ODIR)/%.rel)

#main rule
all: $(ODIR_HEX)/$(TARGET).ihx
#rels
$(ODIR)/%.rel: %.c
	@echo "[compiling] $@"
	@mkdir -p $(dir $@)
	@$(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@
#link
$(ODIR_HEX)/$(TARGET).ihx: $(RELS)
	@echo "[linking] $@"
	@mkdir -p $(dir $@)
	@$(CC) $(CFLAGS) $(MAIN_SRC) $(RELS) $(INCLUDES) -o $@
# Clean rule
clean:
	@rm -rf $(ODIRM)
	@rm -rf $(ODIR_HEX)

Мейк почти такой же как и всегда, только object файлы почему то называется rel файлами. А ещё он может может глючить если постоянно не собирать по новой через make clean / make all.

Код проекта можете найти на gitlab.

Системные функции

Базовые функции для работы с микроконтроллером, это: функция настройки тактирования, функция включения периферии, функция включения и отключения прерываний, и функция установки уровня приоритета прерываний.
Для установки тактирования функция всего одна, она устанавливает частоту 16 МГц от встроенного RC генератора, ни разу не собираюсь в 25 рублёвых проектах использовать кварц.

Скрытый текст

void clockTo16Hsi(void)
{
    // вырубаем делители
    CLK_CKDIVR = HSIDIV_NODIV | CPUDIV_NODIV;
    // мастер-тактирование от внутреннего RC
    CLK_SWR = SWI_HSI;
    // ждём пока пеерключится с таймаутом.
    uint16_t timeout = 65534;
    while( ( CLK_SWCR & SWIF ) || (--timeout > 1) );
    if( timeout < 2 ) {
        CLK_SWCR &= ~SWBSY;
    }
    CLK_SWCR |= SWEN;
}

С активацией периферии так же, как и в STM32, она разбита по двум регистрам. Мой switch-case объединяет их в одну функцию вида: enable(uint8_t periph).
Прерывания в STM8 имеют три уровня приоритета. И имеется функция вида: setPriority(TIM4_ITN, LEVEL1) которой нужно передать номер вектора прерывания и уровень приоритета. Здесь в каждом регистре хранятся уровни приоритета для четырёх прерываний, поэтому функция сначала вычисляет нужный регистр, а потом активирует нужные биты приоритета.

Скрытый текст

void setPriority(uint8_t nInt, uint8_t level)
{
    // ищем нужный регистр
    const uint8_t nIntMax = 29;
    const uint8_t intPerReg = 4;
    if(nInt > nIntMax) return;
    __asm__("rim\n");
    uint8_t reg = nInt / intPerReg;
    uint8_t shift = (nInt % intPerReg)*2;
    // сбрасываем биты приоритета
    ITC_SPR(reg) &= ~(PRIORITY_MSK << shift);
    // устанавливаем биты приоритета
    ITC_SPR(reg) |= (level & PRIORITY_MSK) << shift;
}

За глобальную возможность работы прерываний отвечают биты I0 и I1 регистра CC. Если вы не хотите, что бы какой то определённый участок кода прерывался прерыванием, есть специальные функции их отключения. Поместите ваш атомарный код в скобки из функций uint8_t irq = disableInterrupt(); и enableInterrupt(irq);
Так же для отладки накинул жутко неточный миллисекундный delay.

Работа с GPIO

Порты настраиваются на следующие функции: вход с подтяжкой к питанию или без, с активированным прерыванием или без, выход или выход с открытым истоком на низкой или высокой частоте. Все эти функции объединены в enum, а настройки осуществляются через функцию вида: portConfig(порт, пины, функция), под капотом та же самая работа с регистрами. Так же есть стандартный набор для установки, сброса, инвертирования и считывания пинов.

Скрытый текст

void portConfig(uint8_t port, uint8_t pin, uint8_t config)
{
    switch(config) {
        case INPUT_FLOAT:
            P_DDR(port) &= ~pin;
            P_CR1(port) &= ~pin;
            P_CR2(port) &= ~pin;
            break;
        case INPUT_PULLUP:
    ...

void setPin(uint8_t port, uint8_t pin)
{
    P_ODR(port) |= pin;
}

Отдельно стоит отметить использование портов ввода-вывода для внешних прерываний. Написал отдельную функцию, которая настраивает выбранный порт на вход прерывания с подтяжкой к питанию, а также активирует прерывание по спадающему фронту на этом порту. В общем достаточно вызвать функцию butonInit(порт, пин, приоритет прерывания);, и порт будет сконфигурирован как кнопка по прерыванию.

EEPROM

Уже привык к STM32, в которой для сохранения настроек нужно ставить отельную батарейку, потому все настройки всегда храню тупо во flash. Тут же за сохранение настроек отвечает eeprom. Работа с памятью здесь как и на любых других STMках, нужно сперва её разлочить, записав специальную последовательность ключей, и убедиться в том, что они сработали. А после можно записывать эту память, дождавшись подтверждения, или считывать настройки просто по адресу, как любую другую переменную. У меня для удобства смещение на адрес нулевой ячейки eeprom уже учтено в функциях записи и считывания. Таким образом, вы можете просто нумеровать ваши переменные 0,1,2,3,… не думая о их настоящем адресе. В моей библиотеке есть функции разблокировки, записи и считывания: unlockData(), writeByte(uint8_t addr, uint8_t data), readByte(uint8_t addr).

Скрытый текст

int8_t unlockData()
{
    FLASH_DUKR = FIRST_DATA_KEY;
    FLASH_DUKR = SECOND_DATA_KEY;
    uint8_t tOut = 0xff;
    while( ((FLASH_IAPSR & DUL) == 0) && ((--tOut) > 0) );
    if( tOut == 0 ) {
        return -1;
    }
    return 0;
}

int8_t writeByte(uint8_t addr, uint8_t data)
{
    MMIO8(addr + EEPROM_START) = data;
    uint8_t tOut = 0xff;
    while( ((FLASH_IAPSR & EOP) == 0) && ((--tOut) > 0) );
    if( tOut == 0 ) {
        return -1;
    }
    return 0;
}

uint8_t readByte(uint8_t addr)
{
    return MMIO8(addr + EEPROM_START);
}

Дополнительно имеется функции для записи настроек микроконтроллера. Настройки микроконтроллера, это такие дополнительные байты в eeprom, в которых хранятся конфигурация ремапов, калибровка кварца и активация защиты от копирования прошивки. Работают мои функции доступа к настройкам микроконтроллера аналогично обычным eepromoвским. Пара функций unlockOpt(), lockOpt() для разблокировки/блокировки, и функция записи writeOpt(uint16_t addr, uint8_t option).

Была ещё попытка написать библиотеку для UART, запустить удалось, но оформлена только функция установки битрейта. Для всей остальной периферии есть только регистры.

12 канальный ШИМ

А теперь в стиле того мема про учебники по программированию, научившись устанавливать и сбрасывать один бит на порте ввода вывода. Сразу же научимся управлять программно 4-мя RGB светодиодами. Здесь скажу честно, этот SDCC не очень то оптимизирует код, и классический программный ШИМ со сравнением с пороговым значением нежизнеспособен. Кстати, об оптимизаторе компилятора, сами авторы компилятора советуют пользователям самостоятельно дополнить алгоритмы оптимизации. Потому ШИМ реализован самым иррациональным способом. Просто все состояния регистров портов Px_ODR хранятся в массивах, а цикл, тактируемый таймером передаёт текущее заранее заготовленное состояние порта из массива непосредственно в регистр.

Здесь я на самом деле осознал, что означает часто повторяемая в постах железячников фраза «регистры/разводку портов делали не дураки». Действительно не дураки, ведь благодаря их гениальной разводке 16 портов разбиты не по двум, как могло показаться, а по четырём регистрам. Таким образом, моя дурацкая идея должна потерпеть крах под натиском гениальности умных инженеров, ибо нефиг программные фичи писать всяким там неучам. Идею спасла оптимизация, выводы портов A и B не пересекаются, таким образом для них двоих можно ограничится одним массивом. В итоге, выделив 256 байтные массивы для портов AB, C и D мы занимаем не всю память, а оставляем байт эдак 200 на остальную программу. К слову, как минимум PORTD может быть аппаратным, что может сэкономить ещё 256 байт. Но работоспособными оказались оба варианта, и тот, в котором все 12 каналов программные, и тот, в котором 9 программных и 3 аппаратных.

Общая идея высказана, переходим к схеме и плате. Микроконтроллер в обвязке минимальней некуда, порт прошивки не использутся и выведен на отдельную площадку, кроме ШИМ присутствуют кнопка переключения режимов мигания и UART.Т. к. в будущем это табло не самостоятельное, а вспомогательное устройство bluetooth колоночки. Колоночка будет слать ей коды цветов светодиодов в такт музыке по UART.
Порты ШИМ подключены к светодиодам через самый дещёвый буфер — матрицу ULN2003, светодиоды 1-ваттные, но ток настроен приблизительно на 60 миллиампер на кристалл. С таким током не перегружаются слабые транзисторные сборки и светодиоды так же не требуют охлаждения, да и ток питания не превышает одного ампера. А по яркости — без рассеивателя слегка слепит.

5241f7d1fd253b1db94788af4b0047d5.png

Печатная плата изготовлена утюгом, дорожки от микроконтроллера шириной 0,29, слой всего один, не обошлось без перемычки. Честно говоря я удивился когда с первого раза припаял QFN корпус на ЛУТ плате, могло и не прокатить. Редактор KiCAD.

25ea8c3206a89f74fd589f4f92ff17f4.JPG

Всех особенностей программы описывать не буду, остановлюсь только на функции установки цвета и цикле программного ШИМ.
Начну с функции программного ШИМ, так будет понятнее. В моей программе таймер 2 настроен на отсчет от 0 до 255 с делителем 32. Таким образом, частота ШИМ составит:

F_{16MHz}/256/32 \approx 2kHz

32-х инструкций хватает для того, чтобы передать i-й элемент массива для всех четырёх портов. А после сверить значение счетчика с таймером, и если оно уже не равно, приравнять к значению таймера-счётчика. Это позволяет работать ШИМ стабильно, даже если его будут периодически прерывать прерывания.

void pwmCycle()
{
    static uint8_t prev=0;
    uint8_t gpioab = gpioab_pwm[prev];
    PA_ODR = gpioab&GPIOA_LED_PINS;
    PB_ODR = gpioab&GPIOB_LED_PINS;
    PC_ODR = gpioc_pwm[prev];
//    PD_ODR = gpiod_pwm[prev];
    while(TIM2_CNTRL == prev);
    prev = TIM2_CNTRL;
}

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

Скрытый текст

volatile uint8_t gpioab_pwm[256];
volatile uint8_t gpioc_pwm[256];
//volatile uint8_t gpiod_pwm[256];
volatile uint8_t  pwmPack[12];
volatile uint8_t *pwmPorts[12];
volatile uint8_t  pwmPins[12];

// init pwm arrays
    for(uint8_t i=0 ; i<255 ; i++) {
        gpioab_pwm[i] = 0;
        gpioc_pwm[i] = 0;
//        gpiod_pwm[i] = 0;
    }
    gpioab_pwm[255] = 0;
    gpioc_pwm[255] = 0;
//    gpiod_pwm[255] = 0;
    for(uint8_t i=0 ; i<12 ; ++i) {
        pwmPack[i] = 0;
    }
    pwmPorts[0]  = gpioab_pwm;
    pwmPins[0]   = GPIO_R1;
    pwmPorts[1]  = gpioab_pwm;
    pwmPins[1]   = GPIO_G1;
    pwmPorts[2]  = gpioab_pwm;
    pwmPins[2]   = GPIO_B1;
    pwmPorts[3]  = gpioc_pwm;
    pwmPins[3]   = GPIO_R2;
    pwmPorts[4]  = gpioc_pwm;
    pwmPins[4]   = GPIO_G2;
    pwmPorts[5]  = gpioc_pwm;
    pwmPins[5]   = GPIO_B2;
    pwmPorts[6]  = gpioab_pwm;//gpiod_pwm;
    pwmPins[6]   = GPIO_R3;
    pwmPorts[7]  = gpioc_pwm;
    pwmPins[7]   = GPIO_G3;
    pwmPorts[8]  = gpioc_pwm;
    pwmPins[8]   = GPIO_B3;
    pwmPorts[9]  = gpioab_pwm;
    pwmPins[9]   = GPIO_R4;
    pwmPorts[10] = gpioab_pwm;//gpiod_pwm;
    pwmPins[10]  = GPIO_G4;
    pwmPorts[11] = gpioab_pwm;//gpiod_pwm;
    pwmPins[11]  = GPIO_B4;

Итак, для того, чтобы установить новые значения для любого из каналов нужно заново установить или сбросить бит порта во всех 256 байтах массива. Понятно, что этот процесс можно немного оптимизировать. Рассмотрим случай, когда нужно не поменять скважность с 0 на 255, а увеличить или уменьшить немного. Для этого случая цикл проходит не по всем 256 элементам, а только по тем, что нужно занулить или установить. Для того, чтобы не объявлять 16 битный счётчик цикла, а обойтись восьми битным, наибольший элемент массива рассматривается отдельно от цикла. На отдельные условия для счастливчиков-аппаратных каналов просьба не обращать внимания.

Скрытый текст

void setDuty(uint8_t ch, uint8_t duty)
{
    if( ch == R3 ) {
        TIM2_CCR3L = duty;
        pwmPack[ch] = duty;
        return;
    }
    if( ch == G4 ) {
        TIM2_CCR1L = duty;
        pwmPack[ch] = duty;
        return;
    }
    if( ch == B4 ) {
        TIM2_CCR2L = duty;
        pwmPack[ch] = duty;
        return;
    }
    if(duty>=pwmPack[ch]) {
        (pwmPorts[ch])[duty] |= pwmPins[ch];
    }
    if(duty<=pwmPack[ch]) {
        (pwmPorts[ch])[pwmPack[ch]] &= ~pwmPins[ch];
    }
    for(uint8_t i=pwmPack[ch] ; i<=(duty-1) ; i++) {
        (pwmPorts[ch])[i] |= pwmPins[ch];
    }
    for(uint8_t i=duty ; i<=(pwmPack[ch]-1) ; i++) {
        (pwmPorts[ch])[i] &= ~pwmPins[ch];
    }
    pwmPack[ch] = duty;
}

Теперь об эффектах. Честно говоря, а на 8 светодиодов придумать эффекты гораздо проще, чем на 4. Потому просто выведу градиент и огонь. Эффекты работают на прерываниях, прерывание по таймеру вызывает обновление эффекта с частотой 10 Гц.
Градиент перебирает все цвета в очерёдности красный-пурпурный-синий-морской-зелёный-жёлтый-красный и т.д. Пришлось заморочиться со стартовой позицией, что бы все шесть цветов распределить между 4 светодиодами. Код довольно простой, сюда приводить не буду.
Для огня понадобился 8-битный random. Удивительно, но для восьмибиток предлагется пользоваться 32 битным. Когда я хотел писать RGB лампу на attiny13, я специально для неё написал рандом точно такой же как классический C-шный 32 битный, только 8 битный, разброс значений проверял, нормальный. Эффекты переключаются кнопкой и их можно посмотреть на видео:

© Habrahabr.ru