Начало » Микроконтроллеры » Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.

 
 

Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.

26.03.21   erbol   942   7  

Сегодня мы рассмотрим работу следующих модулей периферии:
• порта ввода-вывода,
• таймера
• аналого-цифрового преобразователя,
• PWM для nRF52832
и, обобщив все полученные знания, реализуем проект «lightControl» для управления состоянием двух светодиодов.

Периферия микроконтроллеров

Как уже говорилось в первой части нашей эпопеи, в плоскости общения МК с внешним миром назначение его периферии состоит в следующем:

а) Порт ввода-вывода позволяет вывести на тот или иной вывод МК или считать с него цифровые данные (логические 1 или 0).
Это может быть обособленная работа одного вывода, в частности с целью:
• включить/выключить светодиод или реле,
• определить состояние кнопки,

Либо — согласованное взаимодействие группы выводов, к примеру для:
• управления шаговым двигателем,
• чтения комбинации состояний ножек энкодера,
• реализации программного варианта одного из протоколов связи (I2c, SPI, UART).

б) Посредством АЦП можно измерить и преобразовать в численное значение уровень аналогового сигнала на соответствующем выводе МК.

в) Таймер обеспечивает своевременность чтения/записи информации с точностью до одного периода его тактового импульса. Дополнительно, таймеры ATtiny85, ATmega8 и STM32F401 можно использовать для генерации аппаратного ШИМ-сигнала. Аналогичная функция для nRF52832 реализуется посредством модуля PWM.

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

Для проверки примеров кода осуществим следующие подготовительные действия:
1. Соберём схему, представленную на Рисунке 1.
Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.
Рисунок 1. Схема устройства проекта lightControl.


Распиновка устройства в зависимости от выбранного вами МК приведена на Рисунке 2.


Рисунок 2. Распиновка устройства проекта lightControl.


2. Создадим папку нового проекта lightControl, куда скопируем шаблонные файлы. Напомню, что актуальный вариант шаблонных хидер-файлов можно скачать из архива в четвёртой части статьи.

3. В папке lightControl создадим парные файлы led.h/led.S, timer.h/timer.S, adc./adc.S и, только для nRF52832, pwm.h/pwm.S. Во вспомогательные хидер-файлы (led.h, timer.h и т. д.) мы будем выносить не системные макроопределения и иную информацию по соответствующему модулю. На начальном этапе использование таких файлов может показаться избыточным, но, поверьте мне, в конечном итоге сэкономит вам немало времени и нервов.

4. Внесём в Makefile следующие изменения и дополнения:
ATtiny85
all:
  avr-as main.S -mmcu=attiny85 -o main.o
  avr-as led.S -mmcu=attiny85 -o led.o
  avr-as timer.S -mmcu=attiny85 -o timer.o
  avr-as adc.S -mmcu=attiny85 -o adc.o
  avr-ld main.o led.o adc.o timer.o -T LinkerScript.ld  -o lightControl.elf
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex
  avrdude -p attiny85 -c usbasp -P usb -e -U flash:w:lightControl.hex:i


ATmega8
all:
  avr-as main.S -mmcu=atmega8 -o main.o
  avr-as led.S -mmcu=atmega8 -o led.o
  avr-as timer.S -mmcu=atmega8 -o timer.o
  avr-as adc.S -mmcu=atmega8 -o adc.o	
  avr-ld main.o led.o timer.o adc.o -T LinkerScript.ld  -o lightControl.elf
  avr-objcopy lightControl.elf -j .text -j .data -O ihex lightControl.hex
  avrdude -p atmega8 -c usbasp -P usb -e -U flash:w:lightControl.hex:i


STM32F401
all:		
  arm-none-eabi-as main.S -mcpu=cortex-m4 -o main.o
  arm-none-eabi-as led.S -mcpu=cortex-m4 -o led.o
  arm-none-eabi-as timer.S -mcpu=cortex-m4 -o timer.o
  arm-none-eabi-as adc.S -mcpu=cortex-m4 -o adc.o
  arm-none-eabi-ld main.o led.o timer.o adc.o -T LinkerScript.ld -o lightControl.elf
  arm-none-eabi-objcopy lightControl.elf -S -O ihex lightControl.hex
  openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "init" -c "reset init" -c "flash write_image erase lightControl.hex" -c "reset" -c "exit"


nRF52832
all:		
  arm-none-eabi-as main.S -mcpu=cortex-m4 -o main.o
  arm-none-eabi-as led.S -mcpu=cortex-m4 -o led.o
  arm-none-eabi-as timer.S -mcpu=cortex-m4 -o timer.o
  arm-none-eabi-as adc.S -mcpu=cortex-m4 -o adc.o
  arm-none-eabi-as pwm.S -mcpu=cortex-m4 -o pwm.o
  arm-none-eabi-ld main.o led.o timer.o adc.o pwm.o -T LinkerScript.ld -o lightControl.elf
  arm-none-eabi-objcopy lightControl.elf -S -O ihex lightControl.hex
  openocd -f interface/stlink.cfg -f target/nrf52.cfg -c "init" -c "reset init" -c "flash write_image erase lightControl.hex" -c "reset" -c "exit"


Как видите, мы включили в процесс компиляции созданные нами ассемблер-файлы led, adc, timer и pwm (для nRF52832). Не смотря на то, что пока указанные файлы — пустые, GCC будет успешно компилировать их в объектные файлы, а затем собирать последние в единый elf-файл.

Кроме того, в Makefile изменены имена elf и hex файлов с main на lightControl. Делать это не обязательно, однако, если все отладочные и прошивочные файлы ваших проектов будут иметь одно и то же имя main, то в конце концов вы запутаетесь в них.

Периферия AVR-8

Поскольку ATtiny85 и ATmega8 располагают разным составом периферии, выберем те из модулей, функционал которых, а также названия регистров и их битов схожи, что позволит объединить объяснения для обоих МК:
• порт ввода-вывода B,
• 8-битный таймер TIMER0 для ATtiny85 и 16-битный таймер TIMER1 для ATmega8,
• канал ADC2 аналого-цифрового преобразователя.

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

Порты ввода-вывода AVR-8

Описание работы портов ввода-вывода ATtiny85 и ATmega8 вы найдёте в Разделе «I/O Ports» (страница даташита 53 и 51, соответственно), информацию же о назначении регистров и их битов — в пункте «Register Description» того же раздела, а также в Таблице «Register Summary».

ATtiny85 в силу своих размеров располагает всего одним портом (порт B), в то время как для ATmega8 их предусмотрено три — порты B, C и D.

За работу порта отвечают три регистра: DDRx, PORTx и PINx, где х — B, C или D.
На Рисунке 3 приведены несколько изменённые и объединённые вместе выдержки из Таблиц «Register Summary» для порта В обоих МК.

Рисунок 3. Адреса и имена битов регистров порта ввода-вывода B МК ATtiny85 и ATmega8.

Обратите внимание, что:
1. Имена битов (PORTBn, DDBn и PINBn) заменены на однообразное РBn, где n — номер бита. Такая замена ничего не меняет в плане корректной записи в регистры и чтения из них, зато наглядно увязывает новые имена с распиновкой МК и втрое уменьшает количество макроопределений номеров битов.

2. Поскольку регистры обоих МК сведены в одну таблиц, информация по битам 6—7 действительна только для ATmega8.

Прежде, чем перейти к назначению регистров и битов порта, добавим макроопределения их имён и абсолютных адресов в шаблонный хидер-файл (attiny85.h или atmega8.h).

/* Адреса и номера битов регистров порта В */
PINB = 0×36
DDRB = 0×37
PORTB = 0×38
  PB7 = 7 /* только для ATmega8 */
  PB6 = 6 /* только для ATmega8 */
  PB5 = 5
  PB4 = 4
  PB3 = 3
  PB2 = 2
  PB1 = 1
  PB0 = 0


Теперь о том, как всё это работает.
а) Регистр DDRB определяет направление работы связанных с его битами выводов МК: если значение бита равно 1, то соответствующий вывод МК будет настроен как выход, в противном случае — как вход.

б) Регистр PORTB имеет двойное назначение:
• если вывод МК настроен как выход, то запись 1 или 0 в связанный с ним бит регистра PORTB обусловит логические 1 или 0, соответственно, на этом выводе.
• если вывод МК настроен как вход, то запись 1 в связанный с ним бит регистра PORTB повлечёт за собой подтяжку вывода к питанию через внутренний резистор сопротивлением около 50 кОм, а 0 — придаст входу состояние Hi-Z с высоким внутренним сопротивлением.

в) Регистр PINB хранит текущее состояние пинов РВn.

Ниже приведён пример кода настройки и записи/чтения для пина РВ0 (вывод 5 и 14 для ATtiny85 и ATmega8, соответственно) с применением макросов из macro.h.

/* Настроить пин PB0 как выход и
   установить в состояние 1 */
setBit DDRB, PB0
setBit PORTB, PB0
/* Настроить пин PB0 как выход и
   установить в состояние 0 */
setBit DDRB, PB0
clearBit PORTB, PB0
/* Настроить пин PB0 как выход и установить
   в состояние, противоположное от текущего */
setBit DDRB, PB0
LDS r16, PORTB
LDI r17, (1 ≪ PB0)
EOR r16, r17
STS PORTB, r16
/* Настроить пин PB0 как вход с подтяжкой к питанию.
   Считать PINB в r16 и проверить состояние PB0.
   Если состояние — низкое, перейти к метке label */
clearBit DDRB, PB0
setBit PORTB, PB0
LDS r16, PINB
ANDI r16, (1 ≪ PB0)
CPI r16, (1 ≪ PB0)
BRNE label
/* Настроить пин PB0 как вход Hi-Z.
   Считать PINB в r16 и проверить состояние PB0.
   Если состояние — высокое, перейти к метке label */
clearBit DDRB, PB0
clearBit PORTB, PB0
LDS r16, PINB
ANDI r16, (1 ≪ PB0)
CPI r16, (1 ≪ PB0)
BREQ label


Пропишем в хидер-файле led.h всю вспомогательную информацию для настройки выводов управления обоими светодиодами из Рисунка 1, а также для включения, выключения и переключения в противоположное состояние жёлтого светодиода.

ATtiny85
.include "attiny85.h"
.include "macro.h"
YELLOW_LED_DDR 	= DDRB
YELLOW_LED_PORT = PORTB
YELLOW_LED      = PB2
GREEN_LED_DDR 	= DDRB
GREEN_LED_PORT  = PORTB
GREEN_LED       = PB0
/* Глобальные функции */
.global ledsInit, yellowLedOn, yellowLedOff, yellowLedToggle


ATmega8
.include "atmega8.h"
.include "macro.h"
YELLOW_LED_DDR 	= DDRB
YELLOW_LED_PORT = PORTB
YELLOW_LED      = PB0
GREEN_LED_DDR 	= DDRB
GREEN_LED_PORT 	= PORTB
GREEN_LED       = PB1
/* Глобальные функции */
.global ledsInit, yellowLedOn, yellowLedOff, yellowLedToggle


Как видите:
а) Директивы .include включения шаблонных хидер-файлов и файла macro.h перенесены в led.h, что способствует уменьшению объёма текста в ассемблер-файле.

б) Для адресов и номеров битов регистров порта B применены двойные макроопределения. Такой подход позволяет, при необходимости, менять выводы управления светодиодами, не затрагивая код в файле led.S.

в) Объявления функций ledsInit, yellowLedOn, yellowLedOff и yellowLedToggle глобальными также вынесены в led.h и оформлены все вместе через запятую одной директивой .global, что разрешено правилами GCC.

Оформим в файле led.S вышеупомянутые функции.

.include "led.h"
  .text
    ledsInit:
      /* Настроить выводы упраления светодиодами как выходы */
      setBit   YELLOW_LED_DDR, YELLOW_LED
      setBit   GREEN_LED_DDR, GREEN_LED
      /* Возврат из функции */
      RET
    yellowLedOn:
      /* Включить жёлтый светодиод */
      setBit   YELLOW_LED_PORT, YELLOW_LED
      /* Возврат из функции */
      RET
    yellowLedOff:
      /* Выключить жёлтый светодиод */
      clearBit YELLOW_LED_PORT, YELLOW_LED
      /* Возврат из функции */
      RET
    yellowLedToggle:
      /* Изменить состояние жёлтого светодиода на противоположное */
      LDS      r18, YELLOW_LED_PORT
      LDI      r19, (1 << YELLOW_LED)
      EOR      r18, r19
      STS      YELLOW_LED_PORT, r18
      /* Возврат из функции */
      RET
.end


А затем в main.S — программу, которая настроит оба вывода управления светодиодами и заставит мигать жёлтый светодиод.

.include  "attiny85.h" /* или "atmega8.h" */
.include "macro.h"
  .data
  .text
    .org   Reset_vector
      RJMP  main			
    .global  main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы упраления светодиодами */
      RCALL  ledsInit				
    main_loop:
      RCALL  yellowLedToggle
      RJMP   main_loop
.end


Вспомним из предыдущей части статьи, что дефолтная частота FCPU тактирования процессора для обоих МК составляет 1 МГц и, ориентируясь на столбец «Cloks» Таблицы «Instruction Set Summary» даташита, прикинем период сигнала на выводе управления жёлтым светодиодом.

В цикле main_loop основной функции присутствуют две инструкции — RCALL (3 тика) и RJMP (2 тика).
Функция yellowLedToggle состоит из следующих инструкций:
LDS — 2 тика
LDI — 1 тик
EOR — 1 тик
STS — 2 тика
RET — 4 тика
Итого 15 тиков.

Время одного тика — 1 / 1 МГц = 1 мкс, следовательно полу-период сигнала будет составлять 15 тиков х 1 мкс = 15 мкс, а период — 30 мкс.

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

Увеличим период сигнала, подключив к проекту таймер.

Таймер AVR-8

Подробности настройки и функционирования таймера TIMERn (сейчас и впредь, n = 0 и 1 для таймера ATtiny85 и ATmega8, соответственно) изложены на странице 65 и 75 даташита для ATtiny85 и ATmega8, соответственно.

Львиная доля обращений при настройке и эксплуатации таймера приходится на 7 регистров:
а) Два регистра контроля TCCRnA и TCCRnB.
б) Два регистра сравнения OCRnA и OCRnB.
в) Счётчик TCNTn.
г) Регистр флагов TIFR.
д) Регистр контроля за прерываниями TIMSK.

Добавим в шаблонные хидер-файлы макроопределения:
• указанных регистров и тех их битов, которые будут упоминаться либо использоваться в примерах ниже (оставшиеся вам придётся вносить самостоятельно, по мере необходимости),
• векторов прерываний таймера TIMERn по переполнению и сравнению с регистром OCRnA.

attiny85.h
/* Таблица векторов */
Reset_vector = 0×00
INT0_vector = 0×01
TIMER0_OVF_vector = 0×05
TIMER0_COMPA_vector = 0×0A
ADC_vector = 0×08

/* Адреса и номера битов регистров таймера TIMER0 */
OCR0B = 0×48
OCR0A = 0×49
TCCR0A = 0×4A
  COM0A1 = 7
  COM0A0 = 6
  COM0B1 = 5
  COM0B0 = 4
  WGM01 = 1
  WGM00 = 0
TCNT0 = 0×52
TCCR0B = 0×53
  CS02 = 2
  CS01 = 1
  CS00 = 0
TIFR = 0×58
  OCF0A = 4
  OCF0B = 3
  TOV0 = 1
TIMSK = 0×59
  OCIE0A = 4
  OCIE0B = 3
  TOIE0 = 1


atmega8.h
/* Таблица векторов */
Reset_vector = 0×00
TIMER1_COMPA_vector = 0×06
TIMER1_OVF_vector = 0×08
TIMER0_OVF_vector = 0×09
ADC_vector = 0×0E

/* Адреса и номера битов регистров таймера TIMER1 */
OCR1B_L = 0×48
OCR1B_H = 0×49
OCR1A_L = 0×4A
OCR1A_H = 0×4B
TCNT1_L = 0×4C
TCNT1_H = 0×4D
TCCR1B = 0×4E
  WGM13 = 4
  WGM12 = 3
  CS12 = 2
  CS11 = 1
  CS10 = 0
TCCR1A = 0×4F
  COM1A1 = 7
  COM1A0 = 6
  COM1B1 = 5
  COM1B0 = 4
  WGM11 = 1
  WGM10 = 0
TIFR = 0×58
  OCF1A = 4
  OCF1B = 3
  TOV1 = 2
TIMSK = 0×59
  OCIE1A = 4
  OCIE1B = 3
  TOIE1 = 2

В первую очередь разберёмся с механизмом тактирования таймера и преобразования FCPU в FPERIPHERAL (в данном случае — в FTIMER). Реализуется он посредством битов CS регистра TCCRnB. Выдержка из Таблицы «Clock Select Bit Description» (страница 80 и 99 даташита для ATtiny85 и ATmega8, соответственно), поясняющая связь между комбинацией указанных битов и значением FTIMER, приведена на Рисунке 4, где clkI/O — и есть FCPU,


Рисунок 4. Выбор делителя таймера TIMERn через биты CS регистра TCCRnB МК ATtiny85 и ATmega8.

Из Рисунка 4 следует, что, пока значение всех битов CS равно нулю (дефолтное значение), тактирование таймера отключено, т. е. последний фактически не работает. Любая другая комбинация указанных битов включает тактирование с частотой FCPU, делённой на соответствующее значение.

Помимо приведённых на Рисунке 4, для обоих МК возможны также комбинации 110 и 111 битов CS для случая, когда таймер тактируется от собственного внешнего источника, подключённого к пину Tn (выводы 7 и 11 для ATtiny85 и ATmega8, соответственно). Данный вариант тактирования таймера выходит за рамки вопросов, рассматриваемых в настоящей статье, однако вы, уверен, сможете самостоятельно разобраться с ним, учитывая что соответствующей информации в сети — предостаточно.

Таймер TIMERn обоих МК поддерживает несколько режимов работы, выбор между которыми осуществляется посредством битов WGM регистров TCCRnА и TCCRnB. Полный набор режимов таймера можно увидеть в Таблице «Waveform Generation Mode Bit Description» на странице 79 и 97 даташита для ATtiny85 и ATmega8, соответственно. Мы же рассмотрим три из них, представленные на Рисунке 5.


Рисунок 5. Отдельные режимы работы таймера TIMERn МК ATtiny85 и ATmega8.

Значения BOTTOM из таблиц на Рисунке 5 для всех режимов таймера обоих МК равны 0, а TOP и MAX — будут приводиться ниже, при рассмотрении каждого режима работы.

Режим таймера Normal

Все биты WGM регистров TCCRnА и TCCRnB равны 0. Данные значения являются дефолтными, т. е. по сбросу/подаче питания таймеры обоих МК и без дополнительных настроек находятся в режиме Normal.

TOP = MAX = 255 и 65535 для ATtiny85 и ATmega8, соответственно.

Счётчик TCNTn таймера в режиме Normal увеличивает своё значение на 1 с каждым поступающим тактовым импульсом и по достижению TOP = MAX, т. е. переполнению, сбрасывается в ноль.
При этом:
а) Будет сгенерировано прерывание таймера по переполнению (timer overflow interrupt), если оно разрешено глобально (через бит I регистра SREG) и локально (посредством бита TOIEn регистра TIMSK).

б) Установится в 1 бит TOVn регистра TIFR, который сбрасывается обратно в 0 либо записью из программы в него единицы, либо автоматически при исполнении обработчика прерывания переполнения.

Режим Normal применяется для отсчёта временных интервалов, равных периоду переполнения счётчика TCNTn, путём отслеживания состояния TOVn, причём делать это можно двумя способами: проверкой в main_loop и через соответствующее прерывание таймера.
Реализуем второй вариант, предварительно посчитав подходящее значение делителя таймера.

Как было сказано выше, дефолтный период одного тика тактового сигнала процессора обоих МК составляет 1 мкс.
Выберем для 8-битного таймера TIMER0 МК ATtiny85 комбинацию 101 битов CS, что даст деление FCPU на 1024. Следовательно на переполнение счётчика таймера потребуется 1 мкс х 1024×255 = 261.12 мс.
Счётчик таймера TIMER1 МК ATmega8 — 16-битный, т. е. переполняется, досчитав до 65535, поэтому для него достаточно значение делителя 8 (комбинация 010 битов CS), который обусловит время переполнения в 1 мкс х 8×65535 = 524.28 мс.

Оформим в файлах timer код работы TIMERn.

ATtiny85
timer.h
.include "attiny85.h"
.include "macro.h"

.global TIMER0_OVF_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER0_OVF_Handler:
      /* Переключить жёлтый светодиод в противоположное состояние */
      RCALL  yellowLedToggle
      /* Возврат из обработчика */		
      RETI

    timerInit:
      /* Разрешить прерывание по переполнению */
      setBit  TIMSK, TOIE0
      /* Включить тактирование с делителем 1024 */
      setBit  TCCR0B, CS00
      setBit  TCCR0B, CS02
      /* Возврат из функции */
      RET
.end


ATmega8
timer.h
.include "atmega8.h"
.include "macro.h"
.global TIMER1_OVF_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER1_OVF_Handler:
      /* Переключить жёлтый светодиод в противоположное состояние */
      RCALL  yellowLedToggle			
      /* Возврат из обработчика */
      RETI

    timerInit:
      /* Разрешить прерывание по переполнению */
      setBit  TIMSK, TOIE1
      /* Включить тактирование с делителем 8 */
      setBit  TCCR1B, CS11		
      /* Возврат из функции */
      RET
.end


А затем перепишем вышеприведённый код в main.S следующим образом:

ATtiny85
.include  "attiny85.h"
.include "macro.h"
  .data

  .text	
    .org Reset_vector
      RJMP   main			
    .org TIMER0_OVF_vector * 2
      RJMP   TIMER0_OVF_Handler
				
    .global main		
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end


ATmega8
.include "atmega8.h"
.include "macro.h"
  .data

  .text	
    .org Reset_vector
      RJMP   main			
    .org TIMER1_OVF_vector * 2
      RJMP   TIMER1_OVF_Handler
				
    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI				
      main_loop:					
      /* Перейти к метке main_loop */			
      RJMP   main_loop
.end


Теперь жёлтый светодиод будет мигать с периодом чуть больше 261.12 мс х 2 = 522.4 мс для ATtiny85 и 524.28 мс х 2 = 1048.56 мс для ATmega8, соответственно.
Чтобы посчитать точное время, вам придётся учесть время исполнения инструкций, как мы это делали выше. Однако, поскольку исполняются они процессором с частотой 1 МГц, общий период сигнала увеличится не намного.

Режим таймера CTC

Комбинация битов WGM регистров TCCRnА и TCCRnB:
• 010 для ATtiny85
• 0100 для ATmega8.

TOP = OCRnA.
MAX = 255 и 65535 для ATtiny85 и ATmega8, соответственно.

В режиме CTC счётчик TCNTn таймера, увеличивая на 1 своё значение с каждым тактовым импульсом, сбрасывается в ноль по достижению TOP (т.е. числа, записанного в регистр OCRnA), вследствие чего:
а) Генерируется прерывание таймера по сравнению (timer compare match interrupt), если установлены в 1 бит I регистра SREG и бит OCIEnA регистра TIMSK.

б) Устанавливается в 1 бит OCFnA регистра флагов TIFR, сбросить в 0 который можно записью в него единицы из программы или автоматически исполнением обработчика прерывания.

Так же, как и Normal, режим СTC можно использовать для отсчёта времени, записывая подходящее число в регистр OCRnA. Тот факт, что значение этого числа можно менять в диапазоне от 0 до максимального значения счётчика TCNTn, предопределяет преимущество режима CTC над Normal, заключающееся в возможности регулировать период счёта с точностью до периода 1 тика тактового сигнала таймера.

Кроме отсчёта времени, через комбинацию 01 битов COM регистра TCCRnА (вариант «Toggle OCnA/OCnB on Compare Match» в Таблице «Compare Output Mode, non-PWM» на странице 78 и 96 даташита для ATtiny85 и ATmega8, соответственно) вы можете реализовать аппаратное изменение состояние пина ОСnА (выводы PB0 и РВ1 для ATtiny85 и ATmega8, соответственно), предварительно настроенного как выход, при каждом обнулении счётчика таймера. Скважность полученного таким образом периодического сигнала на выводе ОСnА не регулируется и всегда будет равна 0.5, не зависимо от частоты сигнала. Полупериод сигнала вычисляется как произведение значения OCRnA на период тактового импульса таймера.

Ниже приведён пример, демонстрирующий оба варианта использования режима СТС таймера, когда:
• Состояние вывода управления жёлтым светодиодом меняется через прерывание по сравнению.
• Состояние вывода управления зелёным светодиодом меняется аппаратно, через настройку битов COM регистра TCCRnА.

ATtiny85
timer.h
.include "attiny85.h"
.include "macro.h"

OCR0A_VALUE = 200

.global	TIMER0_COMPA_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER0_COMPA_Handler:
      /* Переключить жёлтый светодиод в противоположное состояние */
      RCALL   yellowLedToggle
      /* Возврат из обработчика */		
      RETI

    timerInit:
      /* Включить режим CTC */
      setBit  TCCR0A, WGM01
      /* Разрешить сигнал на пине OC0A, т.е. на
         выводе управления зелёным светодиодом */
      setBit  TCCR0A, COM0A0
      /* Записать в OCR0A значение сравнения */
      LDI     r16, OCR0A_VALUE
      STS     OCR0A, r16
      /* Разрешить прерывание по сравнению с OCR1A */
      setBit  TIMSK, OCIE0A
      /* Включить тактирование с делителем 1024 */
      setBit  TCCR0B, CS00
      setBit  TCCR0B, CS02
      /* Возврат из функции */
      RET
.end


main.S
.include "attiny85.h"
.include "macro.h"
  .data

  .text
    .org   Reset_vector
      RJMP   main			
    .org TIMER0_COMPA_vector * 2
      RJMP   TIMER0_COMPA_Handler
				
    .global main		
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end


Учитывая, что в регистр OCR0A записывается число 200, после загрузки программы в МК вы должны получить на выводе OCnA (PB0) управления зелёным светодиодом сигнал со скважностью 0.5 и полупериодом 200×1024×1мкс = 204800 мкс.
Полупериод сигнала на выводе PB2 управления жёлтым светодиодом будет больше на время исполнения с частотой FCPU инструкций обработчика прерывания.

ATmega8
timer.h

.include "atmega8.h"
.include "macro.h"

OCR1A_VALUE = 50000

.global	TIMER1_COMPA_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER1_COMPA_Handler:
      /* Переключить жёлтый светодиод в противоположное состояние */
      RCALL   yellowLedToggle
      /* Возврат из обработчика */
      RETI

    timerInit:
      /* Включить режим CTC */
      setBit  TCCR1B, WGM12
      /* Разрешить сигнал на пине OC1A, т.е.
         на выводе управления зелёным светодиодом */
      setBit  TCCR1A, COM1A0
      /* Записать в OCR1A значение сравнения */
      LDI     r17, hi8(OCR1A_VALUE)
      LDI     r16, lo8(OCR1A_VALUE)
      STS     OCR1AH, r17
      STS     OCR1AL, r16	
      /* Разрешить прерывание по сравнению с OCR1A */
      setBit  TIMSK, OCIE1A					
      /* Включить тактирование с делителем 8 */
      setBit  TCCR1B, CS11
      /* Возврат из функции */
      RET
.end


Обратите внимание на порядок записи числа в регистр OCR1A: сначала производится запись в старший байт регистра, а затем — в младший. Чтение из этих регистров осуществляется в обратном порядке: в первую очередь считывается младший байт и вслед за ним — старший.
Дело в том, что регистры TCNT1, OCR1A/B и ICR1 таймера TIMER1 МК Atmega8 — шестнадцати-битные. Поскольку восьми бит РОН не хватает для обращения к указанным регистрам за один раз, производитель определил строгий порядок доступа к ним, изложенный в Разделе «Accessing 16-bit Registers» на странице 77 даташита.

main.S
.include  "atmega8.h"
.include  "macro.h"
  .data

  .text	
    .org Reset_vector
      RJMP   main			
    .org TIMER1_COMPA_vector * 2
      RJMP   TIMER1_COMPA_Handler

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI				
    main_loop:					
      /* Перейти к метке main_loop */			
      RJMP   main_loop
.end


В случае с ATmega8 регистр OCR1A — двух-байтный, что позволяет записать в него число 50000. С учётом значения 8 делителя тактовой частоты таймера полу-период сигнала на выводе OC1A (PB1) управления зелёным светодиодом составит 1 мкс х 8×50000 = 400 мс.
Полу-период сигнала на выводе PB0 управления жёлтым светодиодом будет, как и в случае с ATtiny85, чуть больше: на время, необходимое для исполнения инструкций обработчика прерывания.

Следует отметить, что для ATmega8 предусмотрен ещё один вариант режима СТС, когда сравниваются значения регистров TCNTn и ICR1. Данная модификация режима используется, в частности, для подсчёта количества изменений состояния пина ICP1 (вывод 14 МК). Настройка и эксплуатация обоих модификаций настолько похожи, что вам не сложно будет реализовать при необходимости вторую, ориентируясь на пример выше.

Режим таймера Fast PWM

Оба МК поддерживают несколько модификаций режима Fast PWM, из которых мы рассмотрим по одной для каждого микроконтроллера, выбираемой посредством комбинации битов WGM регистров TCCRnА и TCCRnB:
• 011 для ATtiny85.
• 0101 для ATmega8.

При выборе вышеуказанной комбинации битов WGM таймер TIMER1 ATmega8 как-бы «урезается» до размеров 8-битного таймера так, что MAX для обоих микроконтроллеров равен 255.
TOP определяется числом, записанным в OCRnA, и может принимать значения от 0 до 255.

Из названия режима понятно, что он позволяет реализовать полноценный ШИМ-сигнал с регулируемой скважностью на пинах OCnA/OCnB микроконтроллера. Осуществляется это посредством битов COM (смотрите Таблицу «Compare Output Mode, Fast PWM» на странице 78 и 96 даташита для ATtiny85 и ATmega8, соответственно). При этом, период ШИМ-сигнала задаётся значением MAX, а скважность — соотношением TOP/MAX.

В режиме Fast PWM по ходу работы счётчика TCNTn таймера происходит несколько событий.

Когда значение счётчика сравнивается со значением числа, записанного в OCRnA:
а) Генерируется прерывание таймера по сравнению, если установлены в 1 бит I регистра SREG и бит OCIEnA регистра TIMSK.
б) Устанавливается в 1 бит OCFnA регистра флагов TIFR.
в) Состояние пинов OCnA/OCnB меняется на противоположное, если разрешена генерация ШИМ-сигнала.

При этом счётчик продолжает считать до значения MAX, лишь по достижению которого сбрасывается в ноль, в связи с чем:
а) Будет сгенерировано прерывание таймера по переполнению, если оно разрешено глобально и локально.
б) Установится в 1 бит TOVn регистра TIFR.
в) Состояние пинов OCnA/OCnB меняется на противоположное, если разрешена генерация ШИМ-сигнала.

Выберем для обоих МК следующие настройки:
• Комбинация битов COM — 10, что разрешит не-инвертированный ШИМ-сигнал на выводе управления зелёным светодиодом.
• Делитель частоты тактирования — 8, обуславливающий период одного тика таймера в 8 мкс.
• Число, записываемое в OCRnA — 50.

Тогда, период ШИМ-сигнала составит 8мкс х 255 = 2040 мкс, время нахождения вывода OCnA в высоком состоянии — 8мкс х 50 = 400 мкс, а скважность — 50 / 255 = 0.196.

Теперь об управлении жёлтым светодиодом.
Чтобы период сигнала на пине YELLOW_LED не зависел от значения OCRnA, из двух возможных выберем прерывание по переполнению. Значение самого периода составит 8мкс х 255×2 = 4080 мкс, что опять делает сигнал невидимым для глаза. Поэтому, уменьшим частоту сигнала посредством одного из свободных РОН (например, r21), оформив обработчик прерывания следующим образом:
1. При каждом прерывании значение r21 увеличивается на единицу.
2. По достижению числа в r21 порогового значения (к примеру, 200), состояния вывода YELLOW_LED переключается на противоположное, а сам r21 — обнуляется.

Фактически, мы разделим частоту сигнала на 200, заставив мигать жёлтый светодиод с полу-периодом 8мкс х 255×200 = 408 мс.

ATtiny85
timer.h

.include "attiny85.h"
.include "macro.h"

OCR0A_VALUE = 50
COUNTER_MAX = 200

.global	TIMER0_OVF_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER0_OVF_Handler:
      /* Увеличить на 1 значение r21 и сравнить с COUNTER_MAX */
      INC     r21
      CPI     r21, COUNTER_MAX
      /* Если  меньше, перейти к метке return */
      BRNE    return
      /* В противном случае, обнулить r21 и переключить */
      /* жёлтый светодиод в противоположное состояние */
      CLR     r21
      RCALL   yellowLedToggle
     return:
      /* Возврат из обработчика */		
      RETI

    timerInit:
      /* Включить режим FastPWM */
      setBit  TCCR0A, WGM01
      setBit  TCCR0A, WGM00
      /* Разрешить ШИМ-сигнал на пине OC0A, т.е. на
         выводе управления зелёным светодиодом */
      setBit  TCCR0A, COM0A1
      /* Записать в OCR0A значение сравнения */
      LDI     r16, OCR0A_VALUE
      STS     OCR0A, r16
      /* Разрешить прерывание по переполнению */
      setBit  TIMSK, TOIE0
      /* Включить тактирование с делителем 8 */
      setBit  TCCR0B, CS01
      /* Возврат из функции */
      RET
.end


main.S
.include  "attiny85.h"
.include "macro.h"
  .data

  .text	
    .org Reset_vector
      RJMP   main
    .org TIMER0_OVF_vector * 2
      RJMP   TIMER0_OVF_Handler
				
    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end


ATmega8
timer.h

.include "atmega8.h"
.include "macro.h"

OCR1A_VALUE = 50
COUNTER_MAX = 200

.global	TIMER1_OVF_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER1_OVF_Handler:
      /* Увеличить на 1 значение r21 и сравнить с COUNTER_MAX */
      INC     r21
      CPI     r21, COUNTER_MAX
      /* Если  меньше, перейти к метке return */
      BRNE    return
      /* В противном случае, обнулить r21 и переключить */
      /* жёлтый светодиод в противоположное состояние */
      CLR     r21
      RCALL   yellowLedToggle
     return:					
      /* Возврат из обработчика */
      RETI

    timerInit:
      /* Включить режим FastPWM, 8-bit */
      setBit  TCCR1B, WGM12
      setBit  TCCR1A, WGM10
      /* Разрешить ШИМ-сигнал на пине OC1A, т.е. на
         выводе управления зелёным светодиодом */
      setBit  TCCR1A, COM1A1
      /* Записать в OCR1A значение сравнения */
      LDI     r17, hi8(OCR1A_VALUE)
      LDI     r16, lo8(OCR1A_VALUE)
      STS     OCR1AH, r17
      STS     OCR1AL, r16
      /* Разрешить прерывание по переполнению */
      setBit  TIMSK, TOIE1		
      /* Включить тактирование с делителем 8 */
      setBit  TCCR1B, CS11
      /* Возврат из функции */
      RET
.end


Как видите, не смотря на то, что число 50, меньше, чем 255, запись производится, согласно упоминавшегося выше Раздела «Accessing 16-bit Registers», в оба байта регистра OCR1A: 0 — в OCR1AH и 50 — в OCR1AL.

main.S
.include  "atmega8.h"
.include  "macro.h"
  .data

  .text
    .org Reset_vector
      RJMP   main
    .org TIMER1_OVF_vector * 2
      RJMP   TIMER1_OVF_Handler

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Разрешить прерывания глобально */
      SEI				
    main_loop:					
      /* Перейти к метке main_loop */			
      RJMP   main_loop
.end


В подобной реализации проект имеет кое-какие недостатки:

• Во-первых, частота ШИМ-сигнала слишком высока, чтобы можно было наблюдать его, не вооружившись осциллографом.

• Во-вторых, чтобы изменить скважность сигнала, придётся в timer.h каждый раз прописывать новое значение OCRnA_VALUE, а затем компилировать программу и загружать её в МК.

Исправим ситуацию, приведя скважность ШИМ-сигнала к зависимости от уровня аналогового напряжение на пине ADC2.

Аналого-цифровой преобразователь AVR-8

Оба рассматриваемые нами МК располагают 10-разрядным аналого-цифровым преобразователем, нюансы настройки и функционирования которого можно узнать из Раздела «Analog to Digital Converter» на странице 122 и 189 даташита, соответственно.

Следует особо отметить, что питание аналоговой части ATmega8 — отдельное, поэтому необходимо соединить между собой оба вывода GND (8 и 22), а также выводы VCC и AVCC (7 и 20).
Для ATtiny85 питание аналоговой и цифровой частей МК — единое.

АЦП ATtiny85 имеет 4 канала ADC0 — ADC3 для измерения уровня аналогового напряжения на выводах 1 — 3 и 7 МК, в то время как для ATmega8 их число составляет 6 — с ADC0 по ADC5 (выводы 23 — 28 МК).

Для нашего проекта достаточно использования трёх регистров АЦП: двух регистров настройки (ADCSRA и ADMUX) и регистра данных ADC, макроопределения адресов и номеров битов которых следует внести в шаблонный хидер-файл.

attiny85.h/atmega8.h
/* Адреса и номера битов регистров модуля АЦП */
ADCL = 0×24
ADCH = 0×25
ADCSRA = 0×26
  ADEN = 7
  ADSC = 6
  ADATE = 5 /* ADFR — для ATmega8 */
  ADIF = 4
  ADIE = 3
  ADPS2 = 2
  ADPS1 = 1
  ADPS0 = 0
ADMUX = 0×27
  ADLAR = 5
  MUX1 = 1


Объявим во вспомогательном хидер-файле adc.h глобальными:
• обработчик прерывания ADC_Handler,
• функцию adcInit инициализации АЦП,
• функцию adcStartConversion однократного измерения уровня аналогового напряжения.

adc.h
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

.global	ADC_Handler, adcInit, adcStartConversion


И оформим их в adc.S.

В обработчике прерывания необходимо лишь копировать содержимое регистра данных ADC аналого-цифрового преобразователя в регистр сравнения OCRnA таймера.

Запуск однократного измерения в осуществляется установкой в 1 бита ADSC регистра ADCSRA.

Теперь о настройке АЦП.
Для начала выберем в качестве рабочего канал ADC2, что согласно Таблицы «Input Channel Selections» (страница 135 и 199 даташита для ATtiny85 и ATmega8, соответственно) реализуется комбинацией 0010 битов MUX регистра ADMUX, т. е. установкой в 1 бита MUX1.

Кроме того, установим в 1 бит ADLAR того же регистра, что обусловит выравнивание 10-битного результата измерения по левому краю регистра данных ADC: 8 старших битов результата будут размещаться в ADCH, а 2 младших — в ADCL.

Подобный выбор продиктован следующими соображениями.
Как вы помните, регистр OCR0A таймера ATtiny85 — 8-битный.
Для таймера ATmega8 мы выбрали 8-битный режим Fast PWM, поэтому в генерации ШИМ-сигнала принимает участие лишь младший байт OCR1AL регистра OCR1A.
Учитывая изложенное, необходимо привести 10-битный результат измерения АЦП в соответствие с восемью битами регистра OCRnA таймера. Можно сделать это с помощью пропорций. Однако, есть более простой путь.
Дело в том, что два младших бита результата измерения АЦП — наименее значащие. Более того, показания реального АЦП обычно скачут, т. е. значения младших разрядов — не стабильные, поэтому мы можем не учитывать их, используя только старшие 8 битов результата.
Таким образом, выровняв результат измерения по левому краю, мы будем использовать лишь значение старшего байта ADCH регистра ADC.

Далее нужно определить источник опорного напряжения посредством комбинации битов REFS того же регистра ADMUX.
Мы воспользуемся комбинацией 00 указанных битов, т. е. выберем в качестве ИОН:

а) Напряжение на выводе питания для ATtiny85.

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

Поскольку комбинация 00 является дефолтной, прописывать в коде обнуление с помощью макроса clearBit битов REFS, значение которых и без того по сбросу/подаче питания равно нулю, не имеет смысла.

Частота FADC тактирования АЦП, как следует из Таблицы «ADC Prescaler Selections» на странице 136 (ATtiny85) и 201 (ATmega8) даташита, как минимум в два раза меньше FCPU. При желании можно ещё уменьшить её, выбрав соответствующую комбинацию битов ADPS регистра ADCSRA. Остановимся на значении 4 делителя, установив в 1 бит ADPS1 указанного регистра.

Остаётся лишь установить в 1 два бита того же регистра ADCSRA:
• Бит ADIE регистра, разрешив тем самым локально прерывание АЦП по завершению измерения,
• Бит ADEN, что обусловит включение АЦП.

Тогда содержимое фалов adc.S и main.S, а также результат работы проекта lightControl будут выглядеть нижеследующим образом.

ATtiny85
adc.S

.include "adc.h"
  .text
    ADC_Handler:
      /* Скопировать в OCR0A значение ADCH */
      PUSH    r17
      LDS     r17, ADCH
      STS     OCR0A, r17
      POP     r17
      /* Возврат из обработчика */
      RETI

    adcInit:
      /* Выбрать канал ADC2(PB4) и установить
         выравнивание данных по левому краю */
      setBit  ADMUX, MUX1
      setBit  ADMUX, ADLAR
      /* Разрешить локально прерывание АЦП */
      setBit  ADCSRA, ADIE
      /* Разрешить тактирование АЦП с частотой F_CPU / 4 */
      setBit  ADCSRA, ADPS1
      setBit  ADCSRA, ADEN
      /* Возврат из функции */
      RET

    adcStartConversion:
      /* Запустить измерение */
      setBit  ADCSRA, ADSC
      /* Возврат из функции */
      RET
.end


main.S
.include "attiny85.h"
.include "macro.h"
  .data

  .text	
    .org Reset_vector
      RJMP   main
    .org TIMER0_OVF_vector * 2
      RJMP   TIMER0_OVF_Handler
    .org ADC_vector * 2
      RJMP   ADC_Handler

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Настроить АЦП */
      RCALL  adcInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end


Видео в работе № 1


Видео 1. Результат работы проекта lightControl для МК ATtiny85.


ATmega8
adc.S

.include "adc.h"
  .text
    ADC_Handler:
      /* Скопировать в OCR1A значение ADCH */
      PUSH    r16
      PUSH    r17
      LDS     r16, ADCL
      LDS     r17, ADCH
      STS     OCR1AH, r16
      STS     OCR1AL, r17
      POP     r17
      POP     r16
      /* Возврат из обработчика */      
      RETI

    adcInit:
      /* Выбрать канал ADC2(PС2) и установить
         выравнивание данных по левому краю */
      setBit  ADMUX, MUX1
      setBit  ADMUX, ADLAR
      /* Разрешить локально прерывание АЦП */
      setBit  ADCSRA, ADIE
      /* Разрешить тактирование АЦП с частотой F_CPU / 4 */
      setBit  ADCSRA, ADPS1
      setBit  ADCSRA, ADEN
      /* Возврат из функции */
      RET

    adcStartConversion:
      /* Запустить измерение */
      setBit  ADCSRA, ADSC
      /* Возврат из функции */
      RET
.end


main.S
.include "atmega8.h"
.include "macro.h"
  .data

  .text
    .org Reset_vector
      RJMP   main
    .org TIMER1_OVF_vector * 2
      RJMP   TIMER1_OVF_Handler
    .org ADC_vector * 2
      RJMP   ADC_Handler

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить выводы управления светодиодами */
      RCALL  ledsInit
      /* Настроить таймер */
      RCALL  timerInit
      /* Настроить АЦП */
      RCALL  adcInit
      /* Разрешить прерывания глобально */
      SEI
    main_loop:
      /* Перейти к метке main_loop */
      RJMP   main_loop
.end


Видео в работе № 2


Видео 2. Результат работы проекта lightControl для МК ATmega8.


Примеры кода для ATtiny85 и Atmega8 выложены в архив в подвале статьи.

Периферия STM32F401

Вся информация о периферии этого МК собрана в Reference manual, за исключением Таблицы 9 «Alternate function mapping», размещённой на странице 44 даташита и содержащей информацию об альтернативных функциях выводов.

Из всего многообразия предусмотренной производителем периферии нами будут рассмотрены:
• Порт ввода-вывода А.
• Таймер общего назначения TIM5.
• Аналого-цифровой преобразователь ADC1.

Как вы помните из Раздела 1.2 предыдущей части статьи, для каждого модуля периферии в карте памяти данных STM32F401 выделен сектор, границы которого представлены в Таблице 1 «STM32F401x register boundary addresses» на странице 38 Reference manual.

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

Выдержки из упомянутых выше таблиц с информацией, имеющей отношение к рассматриваемым нами модулям и выводам, приведена на Рисунке 6.

Рисунок 6. Выдержка из Таблицы 1 Reference manual и Таблицы 9 даташита МК STM32F401.

Порты ввода-вывода STM32F401

Всего для STM32F401 предусмотрено шесть портов: с GPIOA по GPIOH.
За настройку портов отвечает более десятка регистров, полный перечень которых можно увидеть в Разделе «GPIO registers» на странице 158 Reference manual.

Оформим макроопределения и рассмотрим назначение наиболее часто используемых регистров, представленных на Рисунке 7.

Рисунок 7. Некоторые регистры портов ввода-вывода МК STM32F401.

В предыдущей части статьи мы прописали в stm32f401.h макроопределения адресов нижних границ секторов памяти данных, выделенных под порты ввода-вывода, а также смещений адресов отдельных регистров и номеров некоторых битов.
Учитывая, что в проекте lightControl задействованы выводы с номерами 0, 1 и 4 (PA0, PA1 и PA4), добавим в шаблонный хидер-файл макроопределения ещё не внесённых туда регистров из Рисунка 7, а также номеров битов, ответственных за настройку указанных выводов.

В итоге, фрагмент stm32f401.h, касающийся портов ввода-вывода, будет иметь следующий текущий вид.

/* Адреса и номера битов регистров модуля GPIO */
GPIOH = 0×40021C00
GPIOE = 0×40021000
GPIOD = 0×40020C00
GPIOC = 0×40020800
GPIOB = 0×40020400
GPIOA = 0×40020000
  MODER = 0×00
    MODER_15 = 30
    MODER_14 = 28
    MODER_4 = 8
    MODER_1 = 2
    MODER_0 = 0
  PUPDR = 0×0C
    PUPDR_4 = 8
    PUPDR_1 = 2
    PUPDR_0 = 0
  IDR = 0×10
    IDR_4 = 4
    IDR_1 = 1
    IDR_0 = 0
  ODR = 0×14
    ODR_4 = 4
    ODR_1 = 1
    ODR_0 = 0
  AFRH = 0×24
    AFRH_15 = 28
    AFRH_14 = 24
    AFRH_9 = 4
    AFRH_8 = 0
  AFRL = 0×20
    AFRL_7 = 28
    AFRL_6 = 24
    AFRL_4 = 16
    AFRL_1 = 4
    AFRL_0 = 0

Регистр MODER, как уже говорилось в предыдущей части статьи, ответственен за выбор назначения вывода. На настройку каждого пина порта-ввода-вывода выделено по два бита указанного регистра, комбинация значений которых и определяет текущее назначение.
При выборе режима альтернативной функции необходимо, дополнительно к комбинации соответствующих битов регистра MODER, ещё и определить конкретную альтернативную функцию посредством одного из регистров AFR, исходя из данных Таблицы 9 на Рисунке 6.

Ниже приведён пример настроек различных функций для выводов PA0PA4.

/* Включить тактирование модуля GPIOA */
setBit (RCC + AHB1ENR), GPIOAEN
/* Настроить пин PA0 как вход */
LDR r0, =(GPIOA + MODER)
LDR r1, =(0b00 ≪ MODER_0)
STR r1, [r0]
/* Настроить пин PA1 как выход */
LDR r0, =(GPIOA + MODER)
LDR r1, =(0b01 ≪ MODER_1)
STR r1, [r0]
/* Настроить пин PA4 на аналоговый режим */
LDR r0, =(GPIOA + MODER)
LDR r1, =(0b11 ≪ MODER_4)
STR r1, [r0]
/* Настроить пин PA4 на альтернативную функцию */
LDR r0, =(GPIOA + MODER)
LDR r1, =(0b10 ≪ MODER_4)
STR r1, [r0]
/* и выбрать функцию USRT2_CK */
LDR r0, =(GPIOA + AFRL)
LDR r1, =(0b0111 ≪ AFRL_4)
STR r1, [r0]

Регистры PUPDR и IDR понадобятся вам при работе с пином, настроенным как вход: биты первого обуславливают подтяжку входа к питанию или земле через внутренний резистор сопротивлением до 50 кОм, а второго — хранят текущее состояние входа.

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

/* Включить тактирование модуля GPIOA */
setBit (RCC + AHB1ENR), GPIOAEN
/* Настроить пин PA0 как вход */
LDR r0, =(GPIOA + MODER)
LDR r1, =(0b00 ≪ MODER_0)
STR r1, [r0]
/* с подтяжкой к питанию */
LDR r0, =(GPIOA + PUPDR)
LDR r1, =(0b01 ≪ PUPDR_0)
STR r1, [r0]
/* Считать значение IDR в r1 и проверить состояние PA0
   Если состояние — высокое, перйти к метке label */
LDR r0, =(GPIOA + IDR)
LDR r1, [r0]
AND r1, (1 ≪ IDR_0)
CMP r1, 0
BNE label

Соответствующие биты регистра ODR определяют текущее состояние пина, настроенного как выход:

/* Настроить пин PA1 как выход */
LDR r0, =(GPIOA + MODER)
LDR r1, =(0b01 ≪ MODER_1)
STR r1, [r0]
/* и установить в 1 */
setBit (GPIOA + ODR), ODR_1

Оформим в файлах led функции настройки выводов управления зелёным и жёлтым светодиодами, а также установки последнего в разные состояния.
Так же, как и в случае с AVR-8, для гибкости кода применим двойные макроопределения адресов регистров, а с целью минимизации объёма ассемблер-файла вынесем директиву .global во вспомогательный хидер-файл.

led.h
.include "stm32f401.h"
.include "macro.h"

LEDS_GPIO        = GPIOA
LEDS_GPIOEN      = GPIOAEN
YELLOW_LED_MODER = MODER_1
YELLOW_LED_ODR   = ODR_1
GREEN_LED_MODER  = MODER_0
GREEN_LED_ODR    = ODR_0
.global ledsInit, yellowLedOn, yellowLedOff, yellowLedToggle


led.S
.include "led.h"
  .text
    ledsInit:
      PUSH     {LR}
      /* Включить тактирование модуля GPIOA */
      setBit   (RCC + AHB1ENR), LEDS_GPIOEN
      /* Настроить вывод управления жёлтым светодиодом как выход */
      setBit   (LEDS_GPIO + MODER), YELLOW_LED_MODER
      /* Настроить вывод управления зелёным светодиодом
      на альтернативную функцию */
      LDR      r0, = (LEDS_GPIO + MODER)
      LDR      r1, [r0]
      LDR      r2, =(0b10 << GREEN_LED_MODER)
      ORR      r1, r2
      STR      r1, [r0]
      /* и выбрать функцию AF02 - ШИМ канала CH1 таймера TIM5*/
      LDR      r0, =(GPIOA + AFRL)
      LDR      r1, [r0]
      ORR      r1, (0b0010 << AFRL_0)
      STR      r1, [r0]
      /* Возврат из функции */
      POP      {PC}
    yellowLedOn:
      PUSH     {LR}
      /* Включить жёлтый светодиод */
      setBit   (LEDS_GPIO + ODR), YELLOW_LED_ODR
      /* Возврат из функции */
      POP      {PC}
    yellowLedOff:
      PUSH     {LR}
      /* Выключить жёлтый светодиод */
      clearBit (LEDS_GPIO + ODR), YELLOW_LED_ODR
      /* Возврат из функции */
      POP      {PC}
    yellowLedToggle:
      PUSH     {LR}
      /* Изменить состояние жёлтого светодиода на противоположное */
      LDR      r0, = (LEDS_GPIO + ODR)
      LDR      r1, [r0]
      EOR      r1, (1 << YELLOW_LED_ODR)
      STR      r1, [r0]
      /* Возврат из функции */
      POP      {PC}		
.end


Заметьте, что пин PA0 управления зелёным светодиодом настроен на генерацию ШИМ-сигнала. Как реализовать сам сигнал, вы узнаете ниже.

Обеспечим периодическое изменение состояние жёлтого светодиода, откомпилировав и загрузив в МК нижеследующую программу.
.include "stm32f401.h"
.include "macro.h"
  .data

  .text
    .org 0
      /* Указать на вершину стека */
      .word  RAMEND		
    .org Reset_vector	
      .word  main  + 1
			
    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
    main_loop:
      /* Переключить жёлтый светодиод в противоположное состояние */
      BL  yellowLedToggle
      /* Перейти к метке main_loop */
      B   main_loop
.end   


Как вы помните, дефолтная частота FCPU тактирования процессора STM32F401 — 16 МГц, т. е. период одного тика составляет 1 / 16 МГц = 62.5 наносекунд.

Как следует из столбца «Cycles» Таблицы «Instructions Set Summary» на странице 3-30 Cortex M-4 Technical Reference Manual, исполнение инструкций одного цикла основной программы, включая функцию yellowLedToggle, займёт:
BL — 2
PUSH — 2
LDR — 2
LDR — 2
EOR — 1
STR — 2
POP — 2
B — 2
Итого: 15 тиков или 15×62.5 нс = 938 нс

Очевидно, мигание светодиода с такой частотой будет представляться нам постоянным свечением. Учитывая, что время нахождения светодиода во включённом и выключенном состояниях — одинаковое, яркость свечения будет равна половине максимальной.

С помощью таймера увеличим период сигнала на выводе управления жёлтым светодиодом и организуем ШИМ–сигнал на выводе PA0.

Таймер общего назначения TIM5 STM32F401

В состав периферии STM32F401 входят несколько таймеров разного типа, в том числе и таймер общего назначения TIM5, возможности которого мы применим в нашем проекте.
Информация о работе группе таймеров TIM2TIM5 и назначении их регистров изложена на страницах 316—352 и 353—375, соответственно, в Разделе «General-purpose timers (TIM2 to TIM5)» Reference manual.

Таймер TIM5 поддерживает несколько режимов работы, из которых, в рамках проекта lightControl, мы рассмотрим:
• Режим сравнения количества поступивших на таймер тактовых импульсов с заданным числом, при равенстве которых генерируется соответствующее прерывание.
• Режим генерации ШИМ-сигнала на определённом выводе МК, в данном случае на PA0.

Реализовать указанные режимы можно посредством регистров и их битов (отмечены красным), представленных на Рисунке 8.

Рисунок 8. Некоторые регистры группы таймеров TIM2TIM5 МК STM32F401.

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

Для настройки режима сравнения потребуется:
1. Выбрать подходящий делитель частоты, записав его значение в регистр PSC. Остановимся на значении 1600, что даст частоту тактирования таймера FTIMER = FCPU / 1600 = 10 кГц или период одно тика в 100 мкс.

2. Установить посредством записи соответствующего числа (пусть это будет 255) в регистр ARR значение, по достижению которого сбрасывается в ноль счётчик таймера и генерируется прерывание.

Умножив 255 на 100 мкс, мы получим 25.5 мс — значение периода генерации прерывания таймера, а также периода ШИМ-сигнала на выводе управления зелёным светодиодом.

Кроме того, учитывая, что в обработчик прерывания таймера мы будут помещены:
• вызов оформленной выше функции yellowLedToggle,
• вызов предстоящей к оформлению ниже функции запуска измерения АЦП,

указанные 25.5 мс являются ещё и:
• полу-периодом сигнала на выводе управления жёлтым светодиодом,
• периодом сэмплирования АЦП.

Минимальный набор настроек ШИМ-сигнала включает в себя:
1. Выбор одного из двух режимов — PWM1 или PWM2. Мы выберем PWM1 путём записи комбинации 110 в группу битов OC1M регистра CCMR1.

2. Установку скважности, значение которой определяется соотношением чисел, записанных в регистры CCR1 и ARR. Позднее скважность ШИМ-сигнала будет увязана нами с уровнем аналогового напряжения на выводе PA4, пока же запишем в CCR1 число 100, которое даст скважность 100 / 255 = 0.39.

3. Разрешение ШИМ-сигнала на соответствующем выводе МК. В нашем случае такое разрешение для сигнала на пине PA0 достигается установкой в 1 бита CC1E регистра CCER.

После того, как все требуемые настройки произведены, остаётся:
а) Разрешить прерывание таймера глобально (через регистр ISER1 модуля NVIC) и локально (посредством бита UIE регистра DIER).
б) Включить счётчик таймера, записав 1 в бит CEN регистра CR1.

Оформим всё вышеизложенное в файлах timer.
timer.h
.include "stm32f401.h"
.include "macro.h"

TIM5_PSC_VALUE  = 1600
TIM5_ARR_VALUE  = 255
COUNTER_MAX     = 20
PWM_DUTY_CYCLE	= 100

.global TIM5_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIM5_Handler:
      PUSH     {LR}
      /* Очистить флаг прерывания */
      clearBit (TIM5 + TIM_SR), UIF
      /* Увеличить на 1 значение r5 и сравнить с COUNTER_MAX */
      ADD      r5, 1
      CMP      r5, COUNTER_MAX
      /* Если меньше, перейти к метке return */
      BNE      return
      /* В противном случае, обнулить r5 и */
      LDR      r5, =0
      /* изменить состояние жёлтого светодиода на противоположное */
      BL       yellowLedToggle
     return:
      /* Возврат из обработчика */
      POP      {PC}

    timerInit:
      PUSH     {LR}
      /* Включить тактирование таймера TIM5 */
      setBit   (RCC + APB1ENR), TIM5EN
      /* Выбрать значение делителя частоты тактирования */
      LDR      r0, =(TIM5 + TIM_PSC)
      LDR      r1, =TIM5_PSC_VALUE
      STR      r1, [r0]
      /* Установить значение сравнения */
      LDR      r0, =(TIM5 + TIM_ARR)
      LDR      r1, =TIM5_ARR_VALUE
      STR      r1, [r0]
      /* Настроить ШИМ-сигнал канала CH1, в т.ч. */
      /* выбрать режим PWM1*/
      LDR      r0, = (TIM5 + TIM_CCMR1)
      LDR      r1, [r0]
      ORR      r1, (0b110 << OC1M)
      STR      r1, [r0]
      /* установить скважность ШИМ-сигнала */
      LDR      r0, =(TIM5 + TIM_CCR1)
      LDR      r1, =PWM_DUTY_CYCLE
      STR      r1, [r0]
      /* разрешить ШИМ-сигнал на пине PA0 */
      setBit   (TIM5 + TIM_CCER),CC1E
      /* Разрешить прерывание таймера локально */
      setBit   (TIM5 + TIM_DIER), UIE
      /* Разрешить прерывание таймера глобально */
      setBit   ISER1, TIM5_INTERRUPT_BIT
      /* Включить счётчик таймера */
      setBit   (TIM5 + TIM_CR1), CEN
      /* Возврат из функции */
      POP      {PC}
.end


Как видите, чтобы частота мигания жёлтого светодиода была видна для невооружённого глаза, в обработчике прерывания, по аналогии с AVR-8, реализовано её деление на 20, в результате чего период сигнала на пине PA1 составит 25.5 мс х 20×2 = 1.02 секунды плюс пару микросекунд на двойное исполнение инструкций функции yellowLedToggle.

Содержимое файла main.S будет следующим:
.include "stm32f401.h"
.include "macro.h"
  .data

  .text	
    .org 0
      /* Указать на вершину стека */
      .word RAMEND
    .org Reset_vector
      .word main + 1
    .org TIM5_vector
      .word TIM5_Handler + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
      /* Настроить таймер */
      BL  timerInit
    main_loop:
      /* Перейти к метке main_loop */
      B   main_loop
.end


Для полноты, подключим к проекту АЦП микроконтроллера и реализуем зависимость скважности ШИМ-сигнала на пине PA0 от уровня аналогового напряжения на входе PA4.

Аналого-цифровой преобразователь STM32F401

АЦП STM32F401 детально описан в Разделе «Analog-to-digital converter (ADC)» на странице 213 Reference manual.

На рисунке 9 представлены те регистры и их биты, запись в которые обеспечивает настройки АЦП в объёме, достаточном для реализации проекта lightControl.

Рисунок 9. Некоторые регистры модуля АЦП МК STM32F401.

Добавим в шаблонный хидер-файл недостающие макроопределения для модуля АЦП.

stm32f401.h
/* Адреса и номера битов модуля АЦП */
ADC1 = 0×40012000
    ADC_SR = 0×00
      EOC = 1
    ADC_CR1 = 0×04
      RES = 24
      EOCIE = 5
    ADC_CR2 = 0×08
      ADON = 0
      SWSTART = 30
    ADC_SQR3 = 0×34
      SQ1 = 0
    ADC_DR = 0×4C

И пропишем в файлах adc функции adcInit иниициализации АЦП и adcStartConversion запуска разового измерения, а также обработчик прерывания АЦП по завершению измерения.

В функции adcInit:
1. Переведём пин PA4 в аналоговый режим.

2. Включим тактирование модуля АЦП.

3. Выберем канал IN4 для измерения уровня аналогового напряжения, записав комбинацию 100 в группу битов SQ1 регистра SQR3.

4. Установим разрешение АЦП в 8 бит посредством комбинации 10 группы битов RES регистра CR1. Поскольку ранее мы определили для таймера TIM5 максимальное значение счёта так же восьми-битным (255), то выбор указанного разрешения для АЦП позволит напрямую, без использования пропорций, копировать значение регистра данных DR АЦП в регистр CCR1 таймера.

5. Разрешим локально, установив в 1 бит EOCIE регистра CR1, и глобально через регистр ISER0 контроллера NVIC прерывание АЦП по завершению измерения.

6. Запишем единицу в бит ADON регистра CR2, включив тем самым АЦП.

Запуск измерения в функции adcStartConversion обеспечивается записью 1 в бит SWSTART регистра CR2.

В обработчике прерывания достаточно сбросить бит EOC регистра SR, а затем скопировать значение регистра данных DR АЦП в регистр CCR1 таймера.
adc.h
.include "stm32f401.h"
.include "macro.h"

.global	ADC1_Handler, adcInit, adcStartConversion


adc.S
.include "adc.h"
  .text 	
    ADC1_Handler:
      PUSH     {LR}
      /* Очистить флаг прерывания */
      clearBit (ADC1 +  ADC_SR), EOC
      /* Считать значение регистра данных АЦП */
      LDR      r0, =(ADC1 + ADC_DR)
      LDR      r1, [r0]
      /* и записать в регистр CCR1 таймера */
      LDR      r0, =(TIM5 + TIM_CCR1)
      STR      r1, [r0]
      /* Возврат из обработчика */
      POP      {PC}

    adcInit:
      PUSH     {LR}
      /* Разрешить тактирование GPIOA */
      setBit   (RCC + AHB1ENR), GPIOAEN
      /* и перевести пин PA4 в аналоговый режим */
      LDR      r0, =(GPIOA + MODER)
      LDR      r1, [r0]
      LDR      r2, =(0b11 << MODER_4)
      EOR      r1, r2
      STR      r1, [r0]
      /* Включить тактирование ADC1 */
      setBit   (RCC + APB2ENR), ADC1EN
      /* Выбрать канал ADC1_IN4 */
      LDR      r0, =(ADC1 + ADC_SQR3)
      LDR      r1, [r0]
      LDR      r2, =(0b100 << SQ1)
      EOR      r1, r2
      STR      r1, [r0]
      /* Установить разрешение 8 бит */
      LDR      r0, =(ADC1 + ADC_CR1)
      LDR      r1, [r0]
      LDR      r2, =(0b10 << RES)
      EOR      r1, r2
      STR      r1, [r0]
      /* Разрешить прерывание АЦП локально */
      setBit   (ADC1 + ADC_CR1), EOCIE
      /* Разрешить прерывание АЦП глобально */
      setBit   ISER0, ADC1_INTERRUPT_BIT
      /* Включить АЦП */
      setBit   (ADC1 + ADC_CR2), ADON
      /* Возврат из функции */
      POP      {PC}

    adcStartConversion:
      PUSH     {LR}
      setBit   (ADC1 + ADC_CR2), SWSTART
      POP      {PC}
.end


Остаётся лишь добавить в обработчик прерывания таймера (сразу после очистки флага прерывания) вызов функции запуска измерения АЦП:

/* Запустить измерение АЦП */
BL adcStartConversion


Внести соответствующие дополнения в main.S
.include "stm32f401.h"
.include "macro.h"
  .data

  .text
    .org 0
      /* Указать на вершину стека */
      .word  RAMEND		
    .org Reset_vector
      .word  main + 1
    .org ADC1_vector
      .word  ADC1_Handler + 1
    .org TIM5_vector
      .word  TIM5_Handler + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
      /* Настроить АЦП */
      BL  adcInit
      /* Настроить таймер */
      BL  timerInit
    main_loop:
      /* Перейти к метке main_loop */
      B   main_loop
.end


И наблюдать результат работы проекта.

Видео в работе № 3


Видео 3. Результат работы проекта lightControl для МК STM32F401.

В архиве вы найдёте весь приведённый в данной главе код.

Периферия nRF52832

Учитывая, что nRF52832 — не просто МК, а SoC (System on Chip), предназначенный, прежде всего, для создания устройств IoT, состав его периферии достаточно специфичен. Тем не менее, стандартные модули, необходимые для осуществления проекта lightControl, также присутствуют.

Порт ввода-вывода nRF52832

Вся информация о модуле GPIO представлена в одноимённом разделе на странице 111 Product Specification.

Регистры порта ввода-вывода nRF52832, ответственные за настройку участвующих в проекте выводов, а также их расшифровка для вывода P0.02 (для остальных выводов всё — то же самое) представлены на Рисунке 10.

Рисунок 10. Некоторые регистры порта-ввода-вывода МК nRF52832.

Коротко о назначении регистров из Рисунка 10 и их битов.

1. Соответствующий бит регистра DIR определяет направление работы того или иного пина. В частности, запись 0/1 во второй или C-бит настраивает вывод P0.02 как вход/выход, соответственно.

2. Биты регистра OUT обуславливают состояние пина, настроенного как выход.

3. Регистр IN хранит текущее состояние выводов P0.01P0.31.

4. Биты регистра PIN_CNF[N] ответственны за следующие настройки пина N:
• 0-й (или А) бит DIR дублирует для отдельного пина функцию одноимённого регистра,
• 1-й (или B) бит INPUT отключает/подключает буфер для хранения текущего состояния пина, настроенного как вход,
• группа битов С (2—3) PULL позволяет подтянуть пин, настроенный как вход, к питанию или земле через внутренний резистор сопротивлением до 16 кОм,
• группа битов D (8—10) DRIVE определяют значение максимально возможного для данного пина тока,
• биты группы E (16—17) SENSE используется при реализации внешнего прерывания и определяют уровень сигнала на пине, настроенного как вход, при котором генерируется прерывание.

Внесём макроопределения вышеупомянутых регистров и битов из в nrf52832.h:

/* Адреса и номера битов регистров модуля GPIO */
GPIO = 0×50000000
  GPIO_OUT = 0×504
  GPIO_IN = 0×510
  GPIO_DIR = 0×514
  GPIO_PIN_CNF_2 = 0×708
  GPIO_PIN_CNF_25 = 0×764
  GPIO_PIN_CNF_24 = 0×760
    PIN_CNF_DIR = 0
    PIN_CNF_INPUT = 1
    PIN_CNF_PULL = 2
    PIN_CNF_DRIVE = 8
    PIN_CNF_SENSE = 16

Поскольку nRF52832 и STM32F401 созданы на базе одного и того же ядра, глядя на примеры, приведённые выше для последнего и используя те же конструкции, вы, уверен, сможете самостоятельно создать код настройки пинов порта ввода-вывода, установки их в 0/1 или чтения их текущего состояния для nRF52832.

Поэтому, сразу перейдём к оформлению в файлах led функций настройки пинов P0.2425 и управления жёлтым светодиодом.
led.h
.include "nRF52832.h"
.include "macro.h"

YELLOW_LED_PIN     = 24
YELLOW_LED_PIN_CNF = GPIO_PIN_CNF_24
GREEN_LED_PIN      = 25
GREEN_LED_PIN_CNF  = GPIO_PIN_CNF_25

.global	ledsInit, yellowLedOn, yellowLedOff, yellowLedToggle


led.S
.include "led.h"
  .text
    ledsInit:
      PUSH     {r0-r1, LR}
      /* Настроить вывод управления жёлтым светодиодом как выход */
      LDR      r0, =(GPIO + YELLOW_LED_PIN_CNF)
      LDR      r1, =(1 << PIN_CNF_DIR)
      STR      r1, [r0]
      /* Настроить вывод управления зелёным светодиодом как выход */
      LDR      r0, =(GPIO + GREEN_LED_PIN_CNF)
      LDR      r1, =(1 << PIN_CNF_DIR)
      STR      r1, [r0]
      /* Возврат из функции */
      POP      {r0-r1, PC}

    yellowLedOn:
      PUSH     {r0-r1, LR}
      /* Включить жёлтый светодиод */
      setBit   (GPIO + GPIO_OUT), YELLOW_LED_PIN
      /* Возврат из функции */
      POP      {r0-r1, PC}

    yellowLedOff:
      PUSH     {r0-r1, LR}
      /* Выключить жёлтый светодиод */
      clearBit (GPIO + GPIO_OUT), YELLOW_LED_PIN
      /* Возврат из функции */
      POP      {r0-r1, PC}

    yellowLedToggle:
      PUSH     {r0-r1, LR}
      /* Изменить состояние жёлтого светодиода на противоположное */
      LDR      r0, =(GPIO + GPIO_OUT)
      LDR      r1, [r0]
      EOR      r1, (1 << YELLOW_LED_PIN)
      STR      r1, [r0]
      /* Возврат из функции */
      POP      {r0-r1, PC}
.end


Тогда main.S с программой blink для жёлтого светодиода будет выглядеть следующим образом:

.include "nrf52832.h"
.include "macro.h"

  .data

  .text
    .org 0
      /* Указать на вершину стека */
      .word  RAMEND
    .org Reset_vector
      .word  main + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
    main_loop:
      BL  yellowLedToggle
      B   main_loop
.end


Функция yellowLedToggle, как вы наверняка заметили, состоит из абсолютно тех же инструкций, что и для STM32F401, на исполнение которых требуется 15 тиков тактового сигнала процессора.
Вспомним из предыдущей части статьи дефолтную частоту FCPU тактирования процессора nRF52832 и рассчитаем полу-период сигнала на выводе управления жёлтым светодиодом: (1 / 64 МГц) х 15 = 234 наносекунды.

Думаю, вы догадались, что следующим шагом мы подключим к проекту таймер.

Таймер nRF52832

Подробная информация об этом модуле изложены в Разделе «TIMER — Timer/counter» на странице 234 Product Specification.

Всего в состав nRF52832 входит пять таймеров TIMER0TIMER4, смещения адресов и расшифровку некоторых регистров которых вы можете увидеть на Рисунке 11.

Рисунок 11. Некоторые регистры таймеров МК nRF52832.

Настройка и работа — совершенно одинаковые для всех таймеров и отличает их лишь количество регистров сравнения CC, как видно из Таблицы 43 на Рисунке 11.
С учётом этого, остановимся на TIMER2 тем более, что в предыдущей части статьи именно для него мы внесли в шаблонный хидер-файл данные, связанные с прерыванием таймера.

Прежде, чем вникать в детали, внесём данные из Рисунка 11 в nrf52832.h:

/* Адреса и номера битов регистров модуля таймеров */
TIMER2 = 0×4000A000
  TIM_TASKS_START = 0×000
  TIM_TASKS_STOP = 0×004
  TIM_EVENTS_COMPARE_0 = 0×140
  TIM_INTENSET = 0×304
  TIM_INTENCLR = 0×308
    TIM_COMPARE_0 = 16
  TIM_MODE = 0×504
  TIM_BITMODE = 0×508
  TIM_PRESCALER = 0×510
  TIM_CC_0 = 0×540

Как уже говорилось в прошлый раз, дефолтная частота тактирования таймера составляет 16 Мгц. При необходимости её можно делить по формуле
FTIMER = 16 МГц / (2PRESCALER),
записав в регистр PRESCALER соответствующее число.

Выберем в рамках нашего проекта значение PRESCALER равное 3, что даст деление на 8 и уменьшит FTIMER до 2 МГц. Следовательно период одного тика таймера составит 500 нс.

Каждый из пяти таймеров поддерживает два режима работы, выбор между которыми определяется комбинаций двух младших битов регистра MODE:
• Комбинация 00 — режим Timer, в котором счётчик таймера считает до максимального своего значения, т. е. переполнения, сбрасываясь затем в ноль.
• Комбинация 01 — режим Counter для подсчёта количества запущенных задач TASKS_COUNT, которые могут инициироваться из любого места программы: к примеру, в обработчике внешнего прерывания с целью подсчёта общего количества нажатий кнопки.
• Комбинация 10 — тот же режим Counter, но в энергосберегающем варианте.

Нас устроит режим Timer и, поскольку он является дефолтным, оформление его в коде не понадобится.

Максимальное значение счётчика таймера обусловлено его разрядностью, задаваемой, в свою очередь, значением битов 0—1 регистра BITMODE. Разрядность счётчика по сбросу/подаче питания, как видно из Рисунка 11, составляет 16 бит, что вполне устраивает нас ещё и потому, что не потребует дополнительного кода.
Таким образом, время необходимое счётчику для переполнения, составит 500 нс х 65536 = 32.77 мс.

Каждый раз, когда счётчик таймера достигает значения, записанного в регистр сравнения CC[N], может быть сгенерировано соответствующее прерывание, если оно разрешено глобально посредством регистра ISER0 контроллера NVIC и локально установкой в 1 соответствующего бита регистра INTENSET. Запретить локально такое прерывание можно через тот же бит регистра INTENCLR.
Мы воспользуемся лишь одним сравнением — с числом, содержащимся в регистре CC[0]. Причём, в данном случае не важно, какое именно число там находится, поскольку достигать его счётчик будет в любом случае лишь один раз за проход. По сути, будет иметь место аналог прерывания по переполнению для AVR-8.
Учитывая изложенное, запись числа в CC[0] также не требует отражения в программе.

Включение процедуры счёта достигается запуском задачи TASKS_START.

В итоге, содержимое файлов timer будет иметь следующий вид.
timer.h
.include "nRF52832.h"
.include "macro.h"

TIMER2_PRESCALER_VALUE 	= 3
COUNTER_MAX             = 15

.global	TIMER2_Handler, timerInit


timer.S
.include "timer.h"
  .text
    TIMER2_Handler:
      PUSH   {LR}
      /* Сбросить флаг TIM_EVENTS_COMPARE_0 */
      LDR    r0, =(TIMER2 + TIM_EVENTS_COMPARE_0)
      LDR    r1, =0
      STR    r1, [r0]
      /* Увеличить на 1 значение r5 и сравнить с COUNTER_MAX */
      ADD    r5, 1
      CMP    r5, COUNTER_MAX
      /* Если  меньше, перейти к метке return */
      BNE    return
      /* В противном случае, бнулить r5 */
      LDR    r5, =0
      /* и изменить состояние жёлтого светодиода на противоположное */
      BL     yellowLedToggle
     return:
      /* Возврат из обработчика */
      POP    {PC}

    timerInit:
      PUSH   {LR}
      /* Установить делитель 2 ^ 3 = 8 */
      LDR    r0, =(TIMER2 + TIM_PRESCALER)
      LDR    r1, =TIMER2_PRESCALER_VALUE
      STR    r1, [r0]
      /* Разрешить прерывание TIMER2 по совпадению локально */
      setBit (TIMER2 + TIM_INTENSET), TIM_COMPARE_0
      /* Разрешить прерывание TIMER2 глобально */
      setBit ISER0, TIMER2_INTERRUPT_BIT
      /* Включить счётчик */
      LDR    r0, =(TIMER2 + TIM_TASKS_START)
      LDR    r1, = 1
      STR    r1, [r0]
      /* Возврат из функции */
      POP    {PC}
.end


Как вы помните, период переполнения счётчика таймера, а следовательно и генерации прерывания, с учётом произведённых настроек составит 32.77 мс. Такое значение выбрано, исходя из соображений частоты сэмлирования АЦП, который мы подключим к проекту позднее. Однако, для сигнала на жёлтом светодиоде это — слишком маленький период, чтобы быть заметным для глаза. Поэтому, в обработчике прерывания оформлено деление частоты на 15 посредством свободного РОН r5 и макроопределения COUNTER_MAX, что даст полу-период мигания упомянутого светодиода в 32.77 мс х 15 = 491.6 мс.

Осталось переписать код в main.S следующим образом:
.include "nrf52832.h"
.include "macro.h"
  .data

  .text
    .org 0
      /* Указать на вершину стека */
      .word RAMEND
    .org Reset_vector
      .word main + 1
    .org TIMER2_vector
      .word TIMER2_Handler + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
      /* Настроить таймер */
      BL  timerInit
    main_loop:
      B   main_loop
.end


Собственно говоря, ничто не мешает нам организовать ШИМ-сигнал на выводе управления зелёным светодиодом, используя возможности таймера.
Для этого достаточно записать подходящие, не равные друг другу, числа в два регистра сравнения, к примеру CC[0] и CC[1], разрешив глобально и локально прерывания от обоих сравнений.
Далее, в обработчике необходимо выяснять, какое именно событие (EVENTS_COMPARE[0] или EVENTS_COMPARE[1]) стало причиной прерывания и, соответственно, устанавливать в 1 или сбрасывать в 0 вывод P0.25.

Однако, такой вариант содержит ряд недостатков:
а) Процессор будет раз в 32.77 мс отвлекаться от исполнения основной функции для изменения состояния зелёного светодиода.
б) Период ШИМ-сигнала будет неизменным, а сам сигнал — полу-аппаратным, поскольку изменение состояния вывода P0.25 осуществляется из программы, т. е. вручную.

Избежать указанных недостатков позволяет модуль PWM, входящий в состав nRF52832.

Модуль PWM nRF52832

С деталями функционирования и настройки модуля генерации ШИМ-сигналов можно ознакомиться в Разделе «PWM — Pulse width modulation» Product Specification.

В состав nRF52832 входит три одинаковых модуля PWM0PWM2, каждый из которых располагает 4 каналами для генерации сигналов одинаковой частоты, но разной скважности, определяемой пользователем.
Таким образом, вы имеет возможность организовать аппаратный ШИМ-сигнал на 12 пинах МК, настроенных предварительно как выходы.

На Рисунке 12 представлена информация по каналу 0 модуля PWM0, включая расшифровку того минимального количества регистров, которого достаточно для управления яркостью зелёного светодиода.

Рисунок 12. Некоторые регистры модуля PWM0 МК nRF52832.

Добавим макроопределения адресов регистров из Рисунка 12 в nrf52832.h и выясним, как всё это работает.

/* Адреса и номера битов регистров модуля ШИМ */
PWM0 = 0×4001C000
  PWM_TASKS_SEQSTART_0 = 0×008
  PWM_ENABLE = 0×500
    PWM_ENABLE_ENABLE = 0
  PWM_COUNTERTOP = 0×508
  PWM_PRESCALER = 0×50C
  PWM_SEQ_0_PTR = 0×520
  PWM_SEQ_0_CNT = 0×524
  PWM_PSEL_OUT_0 = 0×560
    PWM_PSEL_OUT_PIN = 0
    PWM_PSEL_OUT_CONNECT = 31

Начнём с тактирования. Базовая частота FPWM тактирования всех модулей PWM — такая же, как и у таймеров, т. е. 16 МГц. При желании её можно делить, выбрав соответствующую комбинацию битов 0—2 регистра PRESCALER.

Основу модуля PWM составляет 16-битный счётчик, который может работать в одном из двух режимов счёта в зависимости от значения нулевого бита регистра MODE: 0 — режим Up, 1 — режим UpAndDown.
Нас устроит любой вариант, поэтому остановимся на дефолтном режиме Up, поскольку в этом случае отпадает необходимость отражать данную настройку в коде.
В указанном режиме счётчик считает до максимального своего значения, заданного пользователем посредством регистра COUNTERTOP, а затем сбрасывается в ноль.
При этом дважды инвертируется состояние вывода МК, назначенного, опять же пользователем, как канал ШИМ-сигнала:
• при достижении счётчиком значения 16-битного числа, хранимого по зарезервированному предварительно адресу SRAM,
• при обнулении счётчика.

Таким образом, период ШИМ-сигнала будет равен периоду одного тактового импульса данного модуля PWM, помноженному на значение числа в COUNTERTOP, а скважность — соотношению чисел в SRAM и COUNTERTOP.

В отношении числа, хранимого в SRAM, есть пара ограничений:
• Во-первых, оно не должно быть больше числа в COUNTERTOP, иначе вы получите скважность больше 1.
• Во-вторых, старший бит этого числа определяет инверсию ШИМ-сигнала. Чтобы случайно не изменить инверсию в проектах с переменной скважностью сигнала, лучше подстраховаться и установить максимальную длину в 15 бит для обоих чисел: в COUNTERTOP и SRAM.

В рамках нашего проекта для реализации ШИМ-сигнала на выводе управления зелёным светодиодом достаточно:

1. Настроить пин P0.25 как выход, что мы и сделали выше в функции ledsInit.

2. Выбрать пин P0.25 как канал 0 модуля PWM0, записав его номер 25 в группу битов A (0–4) регистра PSEL.OUT[0]. Кроме того, необходимо соединить между собой указанные канал и пин, сбросив в ноль 31-й бит того же регистра. Однако, поскольку по умолчанию указанный бит и без того равен нулю, в коде данное действие будет нами опущено.

3. Определиться с тактовой частотой модуля PWM0. Выберем деление на 4 до 4 МГц, записав число 2 в регистр PRESCALER, что даст период одного тика в 250 нс.

4. Установить верхнюю границу для счётчика. Чуть позже мы подключим к проекту АЦП с 14-битной разрядностью, поэтому, чтобы не заморачиваться пропорциями, запишем в регистр COUNTERTOP максимально возможное значение 16383 регистра данных АЦП. Тогда, период ШИМ-сигнала составит 250 нс х 16383 = 4.096 мс.

5. Зарезервировать в SRAM место для хранения 16-битного числа. Как вы помните, из второй части статьи, делается это посредством переменной (пусть она называется pwmSeq), объявляемой в секции данных.

6. Загрузить в pwmSeq число, определяющее скважность сигнала. Остановимся на значении 5461, что обусловит скважность 0.33.

7. Указать на переменную pwmSeq, записав значение её адреса в регистр SEQ[0].PTR.

8. Установить количество циклов в последовательности посредством регистра SEQ[0].CNT. Подробнее об этом параметре и последовательностях в целом можете почитать в вышеуказанном раздела Product Specification, мы же запишем в указанный регистр единицу.

9. Разрешить последовательность PWM0, установив в 1 бит ENABLE одноимённого регистра.

10. Запустить генерацию ШИМ-сигнала с помощью задачи TASK_SEQSTART[0].

Оформим эти пункты в файлах pwm.

pwm.h
.include "nRF52832.h"
.include "macro.h"

PWM0_PRESCALER_VALUE = 2
PWM_COUNTERTOP_VALUE = 16383
PWM_INIT_VALUE       = 5461

.global	pwmInit


pwm.S
.include "pwm.h"
  .data
    .global pwmSeq
    pwmSeq: .word 1
  .text
    pwmInit:
      PUSH   {LR}	
      /* Выбрать пин P0.25 как канал 0 модуля PWM0 и подключить его */
      LDR    r0, =(PWM0 + PWM_PSEL_OUT_0)
      LDR    r1, =(25 << PWM_PSEL_OUT_PIN)
      STR    r1, [r0]
      /* Установить  частоту тактирования 4 МГц */
      LDR    r0, =(PWM0 + PWM_PRESCALER)
      LDR    r1, =PWM0_PRESCALER_VALUE
      STR    r1, [r0]			
      /* Установить верхнюю границу счёта */
      LDR    r0, =(PWM0 + PWM_COUNTERTOP)
      LDR    r1, =PWM_COUNTERTOP_VALUE
      STR    r1, [r0]
      /* Загрузить в pwmSeq стартовые параметры ШИМ */
      LDR    r0, =pwmSeq
      LDR    r1, =((0 << 15) | PWM_INIT_VALUE)
      STR    r1, [r0]
      /* Указать адрес переменной pwmSeq */
      LDR    r0, =(PWM0+ PWM_SEQ_0_PTR)
      LDR    r1, =pwmSeq
      STR    r1, [r0]
      /* Установить количество циклов в pwmSeq */
      LDR    r0, =(PWM0 + PWM_SEQ_0_CNT)
      LDR    r1, =1
      STR    r1, [r0]
      /* Разрешить последовательность PWM0 */
      setBit (PWM0 + PWM_ENABLE), PWM_ENABLE_ENABLE
      /* Запустить генерацию ШИМ-сигнала */
      LDR    r0, =(PWM0 + PWM_TASKS_SEQSTART_0)
      LDR    r1, =1
      STR    r1, [r0]
      /* Возврат из функции */
      POP    {PC}
.end


И добавим в main.S инструкцию вызова функции pwnInit:

.include "nrf52832.h"
.include "macro.h"
  .data

  .text
    .org 0
      /* Указать на вершину стека */
      .word RAMEND
    .org Reset_vector
      .word main + 1
    .org TIMER2_vector
      .word TIMER2_Handler + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
      /* Настроить модуль PWM */
      BL  pwmInit
      /* Настроить таймер */
      BL  timerInit
    main_loop:
      B   main_loop
.end


Для наглядности, увяжем скважность ШИМ-сигнала с уровнем аналогового напряжения на пине P0.02.

Аналого-цифровой преобразователь nRF52832

В состав периферии nRF52832 входит 8-канальный АЦП, подробное информацию о котором вы найдёте в Разделе «SAADC» на странице 357 Product Specification.

Альтернативным назначением пина P0.2 является аналоговый вход AIN0, в связи с чем на Рисунке 13 приведена информация именно по этому каналу в объёме, достаточном для завершения проекта lightControl.

Рисунок 13. Некоторые регистры модуля АЦП МК nRF52832.

Выясним назначение отдельных регистров модуля АЦП и их битов.

1. Регистры INTENSET и INTENCLR, как следует из их названия, ответственны за локальные разрешение и запрет, соответственно, прерываний АЦП, набор которых достаточно широк. Для наших целей вполне подойдёт прерывание по событию RESULTDONE, инициируемому после записи результата измерения в SRAM, поэтому в коде установим в 1 третий бит D регистра INTENSET.

2. Установка в 1 нулевого бита ENABLE одноимённого регистра включает АЦП.

3. Регистры CH[N].PSELP и CH[N].PSELN ответственны за подключение положительного и отрицательного, соответственно, полюсов канала АЦП к тому или иному выводу МК.
Дело в том, что для аналого-цифрового преобразователя nRF52832 предусмотрено два режима работы, выбор между которыми осуществляется битами MODE регистра CH[N].CONFIG:
• Single ended, когда отрицательный полюс подключён к земле, а положительный — к одному из пинов AIN.
• Differential, когда оба полюса подключены к двум разным пинам AIN.

В случае, если выбран режим Differential, посредством битов регистров PSELP/PSELN и назначаются выводы подключения обоих полюсов АЦП.
Для нашего проекта подходит режим Single ended, поэтому оставим дефолтное нулевое значение для регистра PSELN, что обусловит состояние Not connected для отрицательного полюса. Положительный же полюс АЦП подключим к пину P0.02 (AIN0), записав число 1 в биты 0—4 регистра PSELP.

4. С помощью регистра CH[0].CONFIG осуществим следующие настройки для канала AIN0:
• Откажемся от подключения внутреннего резистора оставив дефолтное значение битов RESN.
• Остановимся на усилении ¼, записав число 2 в биты GAIN. Тогда, согласно формулы из пункта 37.8 «Reference» вышеуказанного раздела Product Specification, мы получим диапазон измерений от 0 В до напряжения питания микроконтроллера VDD, при условии выбора последнего в качестве источника опорного напряжения.
• Выберем упоминавшийся выше дефолтный режим Single ended, оставив без изменения значения битов MODE.

5. Регистр RESOLUTION позволяет выбрать разрядность АЦП. Запишем число 3 в биты VAL, установив тем самым разрядность в 14 бит.

6. В регистр RESULT.PTR записывается адрес в SRAM, куда помещается результат измерения. По аналогии с модулем PWM мы объявим в секции данных файла adc.S переменную adcValue, адрес которой и запишем в указанный регистр.

7. Регистр RESULT.MAXCNT определяет размер буфера для хранения результатов измерения. Нас вполне устроит число 1.

После осуществления вышеуказанных настроек останется лишь разрешить прерывание АЦП глобально, а обработчике прерывания скопировать число из переменной adcValue в переменную pwmSeq и перезапустить последовательность PWM0.

Внесём в шаблонный хидер-файл необходимые макроопределения.

/* Адреса и номера битов регистров модуля АЦП */
SAADC = 0×40007000
  SAADC_TASKS_START = 0×000
  SAADC_TASKS_SAMPLE = 0×004
  SAADC_EVENTS_STARTED = 0×100
  SAADC_EVENTS_RESULTDONE = 0×10C
  SAADC_INTENSET = 0×304
  SAADC_INTENCLR = 0×308
    SAADC_RESULTDONE = 3
  SAADC_ENABLE = 0×500
      SAADC_ENABLE_ENABLE = 0
  SAADC_CH_0_PSELP = 0×510
  SAADC_CH_0_PSELN = 0×514
      AIN0 = 1
  SAADC_CH_0_CONFIG = 0×518
      GAIN = 8
      REFSEL = 12
  SAADC_RESOLUTION = 0×5F0
  SAADC_RESULT_PTR = 0×62C
  SAADC_RESULT_MAXCNT = 0×630


И оформим всё изложенное выше в файлах adc.
adc.h
.include "nRF52832.h"
.include "macro.h"

SAADC_MAXCNT_VALUE     = 1
SAADC_RESOLUTION_VALUE = 3
SAADS_VALUE_LIMIT      = 16383

.global	SAADC_Handler, adcInit, adcStartConversion


adc.S
.include "adc.h"
  .data	
    .global adcValue
    adcValue: .word 0
  .text
    SAADC_Handler:
      PUSH   {LR}
      /* Сбросить флаг EVENTS_RESULTDONE */
      LDR    r0, =(SAADC + SAADC_EVENTS_RESULTDONE)
      LDR    r1, =0
      STR    r1, [r0]
      /* Скопировать значение  adcValue в pwmSeq */
      LDR    r0, =adcValue
      LDR    r1, [r0]
      LDR    r0, =SAADS_VALUE_LIMIT
      AND    r1, r0
      LDR    r0, =pwmSeq
      STR    r1, [r0]
      /* Перезапустить последовательность PWM0 */
      LDR    r0, =(PWM0 + PWM_TASKS_SEQSTART_0)
      LDR    r1, =1
      STR    r1, [r0]
      /* Возврат из обработчика */
      POP    {PC}

    adcInit:
      PUSH   {LR}
      /* Положительный полюс соединить с пином AIN0(P0.02) */
      LDR    r0, =(SAADC + SAADC_CH_0_PSELP)
      LDR    r1, =AIN0
      STR    r1, [r0]
      /* Разрешение АЦП - 14 бит */
      LDR    r0, =(SAADC + SAADC_RESOLUTION)
      LDR    r1, =SAADC_RESOLUTION_VALUE
      STR    r1, [r0]
      /* Усиление сигнала АЦП - 1/4 */
      LDR    r0, =(SAADC + SAADC_CH_0_CONFIG)
      LDR    r1, =(1 << REFSEL) | (2 << GAIN)
      STR    r1, [r0]
      /* Указать адрес буфера для хранения результатов измерения */
      LDR    r0, =(SAADC | SAADC_RESULT_PTR)
      LDR    r1, =adcValue
      STR    r1, [r0]
      /* Установить размер буфера для хранения результатов измерения */
      LDR    r0, =(SAADC + SAADC_RESULT_MAXCNT)
      LDR    r1, =SAADC_MAXCNT_VALUE
      STR    r1, [r0]
      /* Разрешить локально прерывание АЦП по завершению измерения */
      setBit (SAADC + SAADC_INTENSET), SAADC_RESULTDONE
      /* Разрешить прерывание АЦП глобально */
      setBit ISER0, SAADC_INTERRUPT_BIT
      /* Включить АЦП */
      setBit (SAADC + SAADC_ENABLE), SAADC_ENABLE_ENABLE
      /* Возврат из функции */
      POP    {PC}

    adcStartConversion:	
      PUSH   {LR}
      /* Начать измерение */
      LDR    r0, =(SAADC + SAADC_TASKS_START)
      LDR    r1, =1
      STR    r1, [r0]
     eventsStarted:
      LDR    r0, =(SAADC + SAADC_EVENTS_STARTED)
      LDR    r1, [r0]
      CMP    r1, 0
      BEQ    eventsStarted
      LDR    r0, =(SAADC + SAADC_TASKS_SAMPLE)
      LDR    r1, =1
      STR    r1, [r0]
      /* Возврат из функции */
      POP    {PC}
.end


Обратите внимание на следующие моменты:

а) В обработчике прерывания посредством инструкции AND применяется логическая операция И между текущим и максимально возможным значением уровня аналогового напряжения. Делается это с целью обнулить незначащие старшие биты переменной adcValue, исключив тем самым риски случайной смены инверсии ШИМ-сигнала.

б) Запуск измерения АЦП в функции adcStartConversion реализуется в два этапа: сначала запускается задача TASKS_START и ожидается событие EVENTS_STARTED и лишь затем запускается задача сэмплирования TASKS_SAMPLE.

Сама функция adcStartConversion будет вызываться в обработчике прерывания таймера, который примет следующий окончательный вид:

TIMER2_Handler:
  PUSH {LR}
  /* Сбросить флаг TIM_EVENTS_COMPARE_0 */
  LDR r0, =(TIMER2 + TIM_EVENTS_COMPARE_0)
  LDR r1, =0
  STR r1, [r0]
  /* Запустить измерение АЦП */
  BL adcStartConversion
  /* Увеличить на 1 значение r5 и сравнить с COUNTER_MAX */
  ADD r5, 1
  CMP r5, COUNTER_MAX
  /* Если меньше, перейти к метке return */
  BNE return
  /* В противном случае, обнулить r5 */
  LDR r5, =0
  /* и изменить состояние жёлтого светодиода на противоположное */
  BL yellowLedToggle
 return:
  /* Возврат из обработчика */
  POP {PC}


Для полного завершения проекта необходимо внести дополнения в main.S.

.include "nrf52832.h"
.include "macro.h"
  .data

  .text	
    .org 0
      /* Указать на вершину стека */
      stackPointerInit
    .org Reset_vector
      .word main + 1
    .org SAADC_vector
      .word SAADC_Handler + 1
    .org TIMER2_vector
      .word TIMER2_Handler + 1

    .global main
    main:
      /* Настроить выводы управления светодиодами */
      BL  ledsInit
      /* Настроить модуль PWM */
      BL  pwmInit
      /* Настроить АЦП */
      BL  adcInit
      /* Настроить таймер */
      BL  timerInit
    main_loop:
      /* Перейти к метке main_loop */
      B   main_loop
.end


И можно полюбоваться на результаты своих трудов.

Видео в работе № 4


Видео 4. Результат работы проекта lightControl для МК nRF52832.


Файлы

Весь код настоящей главы, разнесённый по четырём папкам (blink, blinkTimer, blinkTimerPWM и lightControl), выложен в архив.
Файловый сервис недоступен. Зарегистрируйтесь или авторизуйтесь на сайте.



Спасибо за внимание! 🙂
Продолжение следует.


Камрад, рассмотри датагорские рекомендации

🌻 Купон до 1000₽ для новичка на Aliexpress

Никогда не затаривался у китайцев? Пришло время начать!
Камрад, регистрируйся на Али по нашей ссылке. Ты получишь скидочный купон на первый заказ. Не тяни, условия акции меняются.

🌼 Полезные и проверенные железяки, можно брать

Куплено и опробовано читателями или в лаборатории редакции.



 

Читательское голосование

Нравится

Статью одобрили 13 читателей.

Для участия в голосовании зарегистрируйтесь и войдите на сайт с вашими логином и паролем.
 

Поделись с друзьями!

 

 

Связанные материалы

 

Схема на Датагоре. Новая статья Микроконтроллеры AVR семейств Tiny и Mega фирмы ATMEL, Евстифеев А.В.... Издательство: Додэка XXI [М.], 560 стр. 2005 г. Книга посвящена вопросам практического применения...
Схема на Датагоре. Новая статья Микроконтроллеры AVR в радиолюбительской практике. А. В. Белов... А. В. Белов Микроконтроллеры AVR в радиолюбительской практике Данная книга представляет собой...
Схема на Датагоре. Новая статья Ассемблер для микроконтроллера с нуля. Часть 4. Система адресации памяти, назначение выводов, тактирование и прерывания МК... Привет датагорцам! Сегодня мы остановимся на следующих вопросах касательно рассматриваемых нами МК:...
Схема на Датагоре. Новая статья Программирование микроконтроллеров в AtmelStudio 6. Часть 1. Первые шаги... Современное радиолюбительство невозможно представить без микроконтроллеров, и это очевидно. В...
Схема на Датагоре. Новая статья Программа ForMC в помощь программисту микроконтроллеров... Программа называется ForMC, изначально предназначалась для МК AVR. В ней объединены несколько...
Схема на Датагоре. Новая статья Универсальный вольтметр - амперметр на PIC16F676 с открытым программным кодом. Часть 1... Статья предназначена для широкого круга радиолюбителей, желающих освоить азы работы и...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 4... Сегодня рассмотрим программу “бегущих огней” и “бегущих теней”. Примеры “бегущих огней” можно найти...
Схема на Датагоре. Новая статья Elect_60: программа микроконтроллерного управления внешними устройствами от ПК... Многие наши коллеги желающие создать микроконтроллерное устройство, управляемое от ПК сталкиваются...
Схема на Датагоре. Новая статья Программирование микроконтроллеров в AtmelStudio 6. Часть 2. Одна программа на разных языках.... Для радиолюбителей, которые до определенного времени не использовали микроконтроллеры в своих...
Схема на Датагоре. Новая статья Грызем микроконтроллеры. Урок 3.... Эту статью я начну с провокационного вопроса… А какую конструкцию на основе МК хотите создать ВЫ?...
Схема на Датагоре. Новая статья Програмирование в AVR Studio 5 с самого начала. Часть 2... Пишем первую программу!Большинство начинают с мигания светодиодов, и мы не исключение. Если...
Схема на Датагоре. Новая статья Универсальный контроллер управления 7-сегментными LED индикаторами по двум проводам (Atmega16)... Занялся я конструированием нового устройства и встал вопрос — на чем отображать данные....
 

Общаемся по статье 💬

Ассемблер для микроконтроллера с нуля. Часть 5. Периферия МК.

Комментарии, вопросы, ответы, дополнения, отзывы

 

<
Читатель Датагора

standan

Комментарий # 1 от 27-03-21, 22:52.
  • С нами с 17.10.2009
  • 20 комментариев
  • 0 публикаций
 
Огромное спасибо за проделанную работу, практически все разложили по полочкам, респект и уважуха :)

<
Читатель Датагора

erbol

Комментарий # 2 от 01-04-21, 18:16.
  • С нами с 11.12.2014
  • 100 комментариев
  • 15 публикаций
 

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

<
Читатель Датагора

galrad

Комментарий # 3 от 05-04-21, 10:07.
  • С нами с 23.08.2011
  • 104 комментария
  • 12 публикаций
 
Спасибо, Ербол! Детальная проработка даташитов на уровне команд, организация Makefile... Хоть серия статей и называется для начинающих, но уровень уже профессионально инженерный с использованием универсальности. Блин сколько же времени... это требует. И мы уже вплотную приблизились к линуху, где так же организованы программы...

<
Читатель Датагора

erbol

Комментарий # 4 от 05-04-21, 11:15.
  • С нами с 11.12.2014
  • 100 комментариев
  • 15 публикаций
 
Цитата galrad
  Спасибо, Ербол! Детальная проработка даташитов на уровне команд, организация Makefile... Хоть серия статей и называется для начинающих, но уровень уже профессионально инженерный с использованием универсальности. Блин сколько же времени... это требует. И мы уже вплотную приблизились к линуху, где так же организованы программы...

Спасибо, Радик! 🙂
Времени действительно занимает достаточно, особенно перепроверка кода на 4-х разных МК. Но, греет мысль, что кому-то это принесёт пользу.
Да, адепты Linux всегда вызывали у меня уважение своей неизбалованностью в плане ручного труда 🙂

<
Читатель Датагора

Sergiy_83

Комментарий # 5 от 06-04-21, 19:08.
  • С нами с 16.10.2012
  • 85 комментариев
  • 8 публикаций
 
Ербол, спасибо за труд. Ну вы блин даете. Тут на СИ голова трещит, а вы на ассемблере. 💀 Снимаю шляпу.
Мне вот тоже приперло немного. Нужно одну функцию с armv7 переписать на aarch64. Может что нить подскажите, куда копать. В ассемблере ни бумбум. Эта функция применяется в декодере Monkey's Audio (ape) в библиотеке ffmpeg под armv7 . Проблема в том что под архитектуру aarch64 ее никто не написал. Я так понимаю у aarch64 просто другой синтаксис команд. Компилятор ругается на комманды. Эта оптимизация значительно снижает нагрузку на процессор.

#include "libavutil/aarch64/asm.S"

@ scalarproduct_and_madd_int16(/*aligned*/v0,v1,v2,order,mul)
function ff_scalarproduct_and_madd_int16_neon, export=1
        vld1.16         {d28[],d29[]}, [sp]
        vmov.i16        q0,  #0
        vmov.i16        q1,  #0
        vmov.i16        q2,  #0
        vmov.i16        q3,  #0
        mov             r12, r0

1:      vld1.16         {d16-d17}, [r0,:128]!
        vld1.16         {d18-d19}, [r1]!
        vld1.16         {d20-d21}, [r2]!
        vld1.16         {d22-d23}, [r0,:128]!
        vld1.16         {d24-d25}, [r1]!
        vld1.16         {d26-d27}, [r2]!
        vmul.s16        q10, q10,  q14
        vmul.s16        q13, q13,  q14
        vmlal.s16       q0,  d16,  d18
        vmlal.s16       q1,  d17,  d19
        vadd.s16        q10, q8,   q10
        vadd.s16        q13, q11,  q13
        vmlal.s16       q2,  d22,  d24
        vmlal.s16       q3,  d23,  d25
        vst1.16         {q10},     [r12,:128]!
        subs            r3,  r3,   #16
        vst1.16         {q13},     [r12,:128]!
        bne             1b

        vpadd.s32       d16, d0,   d1
        vpadd.s32       d17, d2,   d3
        vpadd.s32       d18, d4,   d5
        vpadd.s32       d19, d6,   d7
        vpadd.s32       d0,  d16,  d17
        vpadd.s32       d1,  d18,  d19
        vpadd.s32       d2,  d0,   d1
        vpaddl.s32      d3,  d2
        vmov.32         r0,  d3[0]
        bx              lr
endfunc

<
Читатель Датагора

erbol

Комментарий # 6 от 06-04-21, 19:35.
  • С нами с 11.12.2014
  • 100 комментариев
  • 15 публикаций
 
Спасибо, Сергей! 🙂

Я ведь тоже 99.99% кода пишу на Си.
Но, вот как на Си объяснить новичку такие вещи как РОН, стек и др., да ещё, чтобы он мог их "пощупать" хотя бы через UART?
Собственно говоря, статья, не смотря на название, не про ассемблер, а про возможность более или менее единого подхода в изучении разных платформ, включая общий компилятор.

Касательно вашего вопроса.
Прежде всего стоит посмотреть ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile
https://montcs.bloomu.edu/Information/ARMv8/ARMv8-A_Architecture_Reference_Manual_(Issue_A.a).pdf
(не знаю, как в комментах прикреплять файлы)

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

<
Читатель Датагора

Sergiy_83

Комментарий # 7 от 06-04-21, 20:09.
  • С нами с 16.10.2012
  • 85 комментариев
  • 8 публикаций
 
Спасибо. Я сам попробую разобраться. Получится - отпишусь.

Информация
Вы не можете участвовать в комментировании. Вероятные причины:
— Администратор остановил комментирование этой статьи.
— Вы не авторизовались на сайте. Войдите с паролем.
— Вы не зарегистрированы у нас. Зарегистрируйтесь.
— Вы зарегистрированы, но имеете низкий уровень доступа. Получите полный доступ.