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

 
 

Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI

27.05.21   erbol   3 352   2  

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

То же самое можно сказать и о некоторых других внешних устройствах: кнопке, терморезисторе, реле и т. д.

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

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

Введение

Обмен восьми-битным числом между двумя устройствами может быть:
а) Параллельным, когда все биты числа передаются/принимаются по 8 линиям одновременно.

б) Последовательным, если биты числа передаются/принимаются по одному. При этом, двунаправленное движение данных может осуществляться как по двум отдельным линиям, так и по одной линии, которую стороны используют по очереди. В частности, в протоколах UART, который мы использовали во второй части нашей серии, и SPI реализован обмен данными по двум линиям, а в I2C — по одной.

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

2. Одна из сторон генерирует общий для обеих тактовый сигнал. Очевидно, для такого обмена, именуемого синхронным, потребуется ещё одна линия — тактирования.

I2C и SPI, рассматриваемые в настоящей части статьи, являются протоколами синхронного последовательного обмена.

Стороны обмена в обоих протоколах принято называть ведущим (master) и ведомым (slave), причём именно ведущий генерирует тактовый сигнал. Чаще всего, микроконтроллер выступает в качестве ведущего и, хотя не исключены и обратные случаи, нас будет интересовать именно эта роль для МК.

Помимо тактирования, ведущий ответственен за:
• Инициирование и завершение сеанса обмена.
• Выбор конкретного ведомого из множества возможных.

В I2C и SPI указанные обязанности реализуются по-разному, поэтому будут рассмотрены нами подробнее ниже.

Большинство микроконтроллеров имеют в своём составе модули аппаратной реализации I2C и SPI, когда необходимо лишь определить параметры протокола (к примеру, скорость обмена или порядок передачи данных), записав требуемые числа в регистры настроек соответствующего модуля, а дальше МК будет автоматически поддерживать обмен данными должным образом.

Но, бывают случаи, когда выбор аппаратного варианта протокола затруднён:
а) Не все МК его поддерживают. В частности, в ATTiny85 отсутствуют полноценные модули указанных протоколов.

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

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

Так же, как правильное произношение слов на том или ином языке не означает вашу способность выстраивать предложения и коммуницировать с носителем языка, способность МК передавать или принимать байт по I2C или SPI не означает удачный обмен данными с ведомым, поскольку в обоих протоколах имеет значение и порядок передачи/приёма байтов. Учитывая изложенное, в практической части нами будет рассмотрено общение микроконтроллера с часами реального времени DS1307 (протокол I2C) и трансивером nRF24L01 (протокол SPI).

Алгоритмы обмена байтом данных

Прежде, чем перейти к специфическим особенностям I2C и SPI, обсудим общие для них алгоритмы передачи/приёма ведущим одного байта данных, предварительно договорившись о нижеследующем:
а) Примем имена DATA и CLOCK для линий данных и тактирования, соответственно.

б) Чуть позже мы оформим код, обеспечивающий установку той или иной линии в высокое/низкое состояние, а пока будем считать, что такая возможность у нас есть.

в) Поскольку большинство устройств поддерживают обмен, начинающийся со старшего бита числа, будем придерживаться того же порядка.

г) Воспользуемся для хранения передаваемого и принимаемого числа двумя произвольными РОН, например r0 и r1.

Тогда, алгоритм передачи 8-битного числа будет выглядеть следующим образом:
1. Загрузить в r0 передаваемое число.

2. Поверить старший бит передаваемого числа и, если он равен 0 установить линию DATA в низкое состояние, в противном случае — в высокое. Осуществляется указанная проверка посредством применения логической операции И между r0 и числом 0b10000000 (0x80 – в шестнадцатеричной системе счисления), результатом которой будет либо 0х00, означающее равенство старшего бита нулю, либо 0х80, если значение старшего бита — 1.

3. Обеспечить тактовый импульс на линии CLOCK, установив её последовательно в высокое и низкое состояние.

4. Сдвинуть r0 влево на один бит, подготовив тем самым к передаче следующий бит.

5. Перейти к п.2, пока не будут переданы все 8 бит.

Обратите внимание, что линия DATA не устанавливается в 0 после завершения тактового импульса (т.е. после п.3), как это происходит в классическом случае. Такой подход не влияет на корректность передачи данных, позволяя при этом сократить объём кода. Отличие будет выражаться лишь в форме логического сигнала на линии DATA, что продемонстрировано на Рисунке 1 для случая передачи числа 255 (0b11111111 – в двоичном представлении).

Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI
Рисунок 1. Форма логического сигнала на линии DATA при различных вариантах передачи.

Для приёма байта потребуется:
1. Сдвинуть r1 влево на один бит для чтения следующего бита.

2. Начать тактовый импульс, установив линию CLOCK в высокое состояние.

3. Проверить состояние линии DATA, а следовательно значение принимаемого бита. Если значение — 1, установить нулевой бит r1 в 1, посредством логической операции ИЛИ между r1 и числом 0b00000001 (0x01 – в шестнадцатеричной системе счисления).

4. Завершить тактовый импульс, установив линию CLOCK в низкое состояние.

5. Перейти к п.1, пока не будут приняты все 8 бит.

Изменение значений r0/r1 при передаче/приёме числа 170 (0b10101010 – в двоичном представлении) можно увидеть на Рисунке 2.

Рисунок 2. Значение РОН r1/r2 для случая передачи/приёма числа 170.


Протокол I2C

Прежде, чем перейти непосредственно к изложению материала, хочу выразить признательность Игорю Карпунину (Нижний Тагил, Россия), который помог развеять туман в моей голове относительно электрической составляющей этого протокола.

Для последующего оформления кода откроем папку нового проекта RTC и:
• скопируем в неё шаблонные файлы, а также объектные файлы uart.o и delayAVR.o (или delayCortex.o) из архива второй части статьи,
• создадим дополнительно к шаблонным файлы I2c.S/I2c.h и ds1307.S/ds1307.h.

Управление линиями протокола I2C

На Рисунке 3 представлена сеть I2C, состоящая из ведущего и двух ведомых.

Рисунок 3. Сеть I2C.
где,
SDA и SCL — выводы, а также линии данных и тактирования, соответственно,
• номинал резисторов R1 и R2, подтягивающих обе линии к питанию может варьироваться в пределах нескольких кОм в зависимости от протяжённости линии.

Проясним, как именно управлять линиями SDA/SCL и какие дополнительные условия в этом аспекте имеются для ведущего:
а) В отсутствие обмена линии должны находится в высоком состоянии, для чего достаточно настроить оба вывода МК как вход.

б) Обеспечить низкое состояние на линии SDA или SCL можно лишь в случае, если соответствующий вывод микроконтроллера настроен как выход в состоянии 0.

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

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

Правила обмена данными по протоколу I2C

1. Любой сеанс обмена данными инициируется и завершается ведущим посредством соответствующих условий:
Start (условное обозначение — S), когда линия SDA переводится в низкое состояние при высоком состоянии линии SCL,
Stop (условное обозначение — P), когда линия SDA переводится в высокое состояние при высоком состоянии линии SCL.

2. В некоторых случаях для обмена может потребоваться условие повторного старта (Repeated Start, условное обозначение — Sr), которое реализуется последовательной установкой линий SDA и SCL в высокое состояние, а затем (после некоторой задержки) — в низкое.

3. Обмен байтом осуществляется за 9 тактовых импульсов на линии SCL, включая:
• 8 бит данных передающей стороны,
• 1 бит ответа принимающей стороны.

4. Ответ (Acknowledge) принимающей стороны может иметь одно из двух значений:
а) ACK (условное обозначение — A), означающее успешный приём 8 битов данных. Реализуется такой ответ посредством установки линии SDA в низкое состояние.
б) NACK (условное обозначение — A с чёрточкой сверху), когда линия SDA остаётся подтянутой к питанию, т.е. в высоком состоянии. Подобный ответ может быть получен в случаях, если принимающая сторона:
• общается с третьей стороной, неисправна либо вообще отсутствует,
• не нуждается более в данных и готова к завершению обмена.

В качестве примера на Рисунке 4 приведена временная развёртка:
а) передачи числа 170 от ведущего к ведомому,
б) приёма ведущим числа 170 от ведомого.

Рисунок 4. Временная развёртка передачи/приёма числа 170.

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

2. Передать 8 битов данных в соответствии с алгоритмом из Раздела 2.

3. После передачи 8-го бита данных освободить линию SDA.

4. Начать 9-й тактовый импульс, установив линию SCL в высокое состояние.

5. Определить состояние линии SDA, т.е. значение ответа ведомого. Обычно, если вы уверены в наличии и исправности ведомого, его ответ остаётся без использования.

6. Завершить 9-й тактовый импульс, установив линию SCL в низкое состояние.

7. Установить линию SDA в низкое состояние, тем самым подготовив её к осуществлению условия Stop.

8. Сформировать условие Stop.

Процедура приёма ведущим байта данных будет выглядеть следующим образом:
1. Сформировать условие Start.

2. Освободить линию SDA.

3. Принять 8 битов данных в соответствии с алгоритмом из Раздела 2.

4. В зависимости от значения ответа ведомому установить линию SDA в высокое (ответ – NACK) или низкое (ответ – ACK) состояние.

5. Обеспечить 9-й тактовый импульс на линии SCK для передачи ответа ведомому.

6. Установить линию SDA в низкое состояние, тем самым подготовив её к осуществлению условия Stop.

7. Сформировать условие Stop.

Как уже говорилось выше, из множества участников сети обмена ведущий должен иметь возможность выбора того или иного ведомого. Для протокола I2C такой выбор осуществляется посредством уникального адреса ведомого, присваиваемого производителем, поэтому любое обращение ведущего к сети начинается с передачи байта содержащего указанный адрес. Если значение полученного ответа — ACK, значит ведомый с таким адресом присутствует, исправен и готов к обмену.

В частности, I2C-адрес RTC DS1307 – 0x68 (1101000 — в двоичном представлении).

Тайминг протокола I2C

Помимо рассмотренных выше правил обмена, протокол I2C регламентирует ещё и временные интервалы на каждом этапе процесса, т.е. его тайминг.
К примеру, требования по времени для DS1307 согласно даташита отображены на Рисунке 5.

Рисунок 5. Тайминг протокола обмена RTC DS1307.

Пусть вас не пугает перспектива оформлять в коде все указанные на Рисунке 5 задержки, поскольку на частотах тактирования FCPU до 64 МГц, т.е. для всех рассматриваемых нами МК, наносекундные задержки обеспечиваются за счёт исполнения микроконтроллером текущих инструкций.

Для всех остальных случаев мы можем прописать функцию задержки I2cDelay длительностью исполнения около 5 мкс, которую будем вызывать при формировании:
• условий Start, Stop и Repeated Start.
• тактового импульса.

Код протокола I2C

Выберем любые два пина МК для осуществления функций выводов SCL и SDA, к примеру:
- PB0 и PB1 для AVR-8,
- P0.24 и P0.25 для nRF52832,
- PB1 и PB0 для STMF401.

И оформим в файле I2c.h
а) макроопределения:
• чисел 0x80 и 0х01 для определения значений старшего и младшего битов байта обмена,
• значений ответов ACK и NACK принимающей стороны,
• количества пустых циклов для обеспечения задержки в 5 мкс функцией I2cDelay.

б) макросы для управления линиями SDA и SCL.

в) прототипы функций и переменных, необходимых для реализации протокола.

AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

I2C_DDR       = DDRB
I2C_PORT      = PORTB
I2C_PIN       = PINB
SCL           = PB0
SDA           = PB1

I2C_DELAY_MAX = 1
MSBit         = 0x80
LSBit         = 0x01
ACK           = 0
NACK          = 1

.macro SCL_HIGH
  clearBit I2C_DDR, SCL
.endm

.macro SCL_LOW
  setBit   I2C_DDR, SCL
.endm

.macro SDA_HIGH
  clearBit I2C_DDR, SDA
.endm

.macro SDA_LOW
  setBit   I2C_DDR,SDA
.endm

.global I2cInit, I2cStart, I2cRestart, I2cStop, I2cDelay, I2cSendByte, I2cReceiveByte
.global ackSlave, ackMaster, dataSend, dataReceive

STM32F401
.include "stm32f401.h"
.include "macro.h"

SDA           = 0
SCL           = 1

I2C_DELAY_MAX = 5
MSBit         = 0x80
LSBit         = 0x01
ACK           = 0
NACK          = 1

.macro SCL_HIGH
  LDR  r0, =(GPIOB + MODER)
  LDR  r1, [r0]
  AND  r1, ~(1 << MODER_1)
  STR  r1, [r0]
.endm

.macro SCL_LOW
  LDR  r0, =(GPIOB + MODER)
  LDR  r1, [r0]
  ORR  r1, (1 << MODER_1)
  STR  r1, [r0]
.endm

.macro SDA_HIGH
  LDR  r0, =(GPIOB + MODER)
  LDR  r1, [r0]
  AND  r1, ~(1 << MODER_0)
  STR  r1, [r0]
.endm

.macro SDA_LOW
  LDR  r0, =(GPIOB + MODER)
  LDR  r1, [r0]
  ORR  r1, (1 << MODER_0)
  STR  r1, [r0]
.endm

.global I2cInit, I2cStart, I2cRestart, I2cStop, I2cDelay, I2cSendByte, I2cReceiveByte
.global ackSlave, ackMaster, dataSend, dataReceive

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

SCL           = 24
SDA           = 25

I2C_DELAY_MAX = 25
MSBit         = 0x80
LSBit         = 0x01
ACK           = 0
NACK          = 1

.macro SCL_HIGH
  LDR  r0, =(GPIO + GPIO_PIN_CNF_24)
  LDR  r1, =(0 << PIN_CNF_DIR)
  STR  r1, [r0]
  LDR  r0, =(GPIO + GPIO_PIN_CNF_24)
  LDR  r1, =(3 << PIN_CNF_PULL)
  STR  r1, [r0]	
.endm

.macro SCL_LOW
  LDR  r0, =(GPIO + GPIO_PIN_CNF_24)
  LDR  r1, =(1 << PIN_CNF_PULL)
  STR  r1, [r0]
  LDR  r0, =(GPIO + GPIO_PIN_CNF_24)
  LDR  r1, =(1 << PIN_CNF_DIR)
  STR  r1, [r0]
.endm

.macro SDA_HIGH
  LDR  r0, =(GPIO + GPIO_PIN_CNF_25)
  LDR  r1, =(0 << PIN_CNF_DIR)
  STR  r1, [r0]
  LDR  r0, =(GPIO + GPIO_PIN_CNF_25)
  LDR  r1, =(3 << PIN_CNF_PULL)
  STR  r1, [r0]
.endm

.macro SDA_LOW
  LDR  r0, =(GPIO + GPIO_PIN_CNF_25)
  LDR  r1, =(1 << PIN_CNF_PULL)
  STR  r1, [r0]
  LDR  r0, =(GPIO + GPIO_PIN_CNF_25)
  LDR  r1, =(1 << PIN_CNF_DIR)
  STR  r1, [r0]
.endm

.global I2cInit, I2cStart, I2cRestart, I2cStop, I2cDelay, I2cSendByte, I2cReceiveByte
.global ackSlave, ackMaster, dataSend, dataReceive


Манипуляции с битами PULL регистра PIN_CNF в случае с nRF52832 обусловлены особенностями чтения значения регистра IN порта ввода-вывода этого МК.

Тогда, код функций протокола в I2c.S будет иметь следующий вид:

AVR-8
.include "I2c.h" 
  .data		
    ackSlave: .byte 0
    ackMaster: .byte 0
    dataSend: .byte 0
    dataReceive: .byte 0
  .text
    I2cInit:
      /* Установить обе линии в высокое состояние */
      SDA_HIGH
      SCL_HIGH
      /* Вернуться из функции */
      RET

    I2cStart:
      /* Установить обе линии в низкое состояние */
      SDA_LOW
      SCL_LOW
      RCALL  I2cDelay
      /* Вернуться из функции */
      RET

    I2cRestart:
      /* Установить обе линии в высокое состояние */
      SDA_HIGH
      SCL_HIGH
      RCALL  I2cDelay
      /* Установить обе линии в низкое состояние */
      SDA_LOW
      SCL_LOW
      RCALL  I2cDelay
      /* Вернуться из функции */
      RET		

    I2cStop:
      /* Установить обе линии в низкое состояние */
      SCL_LOW
      SDA_LOW
      /* Установить обе линии в высокое состояние */
      SCL_HIGH
      SDA_HIGH
      RCALL  I2cDelay
      /* Вернуться из функции */
      RET

    I2cDelay:
      LDI    r16, I2C_DELAY_MAX
     I2cDelayLoop:
      DEC    r16
      BRNE   I2cDelayLoop
      /* Вернуться из функции */
      RET

    I2cSendByte:
      /* Загрузить в r20 количество бит - 8 */
      LDI    r20, 8
      /* Загрузить в r21 передаваемое число из переменной dataSend */
      LDS    r21, dataSend
     sendBit:
      /* Проверить старший бит передаваемого числа */
      LDI    r16, MSBit
      AND    r16, r21
      /* если равен 0, перейти к метке sdaLow */
      BREQ   sdaLow
      /* в противном случае - установить линию SDA в высокое состояние */
      SDA_HIGH
      /* и перейти к метке sclTick */
      RJMP   sclTick
     sdaLow:
      /* установить линию SDA в низкое состояние */
      SDA_LOW
     sclTick:
      /* Обеспечить тактовый импульс на линии SCL */
      SCL_HIGH
      RCALL  I2cDelay
      SCL_LOW
      RCALL  I2cDelay
      /* Сдвинуть влево число в r21 */
      LSL    r21
      /* Проверить, переданы ли все 8 бит */
      DEC    r20
      /* если нет, перейти к метке sendBit */
      BRNE   sendBit
      /* Проверить ответ ведомого, для чего  */
      /* освободить линию SDA */
      SDA_HIGH
      /* начать тактовый импульс */
      SCL_HIGH
      RCALL  I2cDelay
      /* проверить состояние линии SDA */
      LDS    r16, I2C_PIN
      ANDI   r16, (NACK << SDA)
      /* если ответ - NACK, перейти к метке noAck */
      BRNE   noAck
      /* в противном случае - завершить тактовый импульс */
      SCL_LOW
      /* и сохранить в переменной ackSlave значение ACK */
      LDI    r16, ACK
      STS    ackSlave, r16
      /* Вернуться из функции */
      RET
     noAck:
      /* завершить тактовый импульс */
      SCL_LOW
      /* и сохранить в переменной ackSlave значение NACK */
      LDI    r16, NACK
      STS    ackSlave, r16
      /* Вернуться из функции */
      RET

    I2cReceiveByte:
      /* Загрузить в r20 количество бит - 8 */
      LDI    r20, 8
      /* Освободить линию SDA */
      SDA_HIGH
     receiveBit:
      /* Сдвинуть на 1 бит влево число в r21 для чтения следующего бита */
      LSL    r21
      /* Начать тактовый импульс */
      SCL_HIGH
      RCALL  I2cDelay
      /* Определить значение принятого бита */
      LDS    r16, I2C_PIN
      ANDI   r16, (1 << SDA)
      /* Если значение равно 0, перейти к метке tickEnd */
      BREQ   tickEnd
      /* в противном случае - установить в 1 нулевой бит r21 */
      ORI    r21, LSBit
     tickEnd:
      /* Завершить тактовый импульс */
      SCL_LOW
      RCALL  I2cDelay
      /* Проверить приняты ли все 8 бит */
      DEC    r20
      /* если нет, перейти к метке receiveBit */
      BRNE   receiveBit
      /* Установить значение ответа ведомому, для чего */
      /* проверить значение переменной ackMaster */
      LDS    r16, ackMaster
      CPI    r16, NACK
      /* если значение ack - NACK, перейти к метке setNack */
      BREQ   setNack
      /* в противном случае, установить линию SDA в низкое состояние */
      SDA_LOW
      /* и обеспечить тактовый импульс */
      SCL_HIGH
      RCALL  I2cDelay
      SCL_LOW			
      /* Скопировать в переменную dataReceive значение r21 */
      STS    dataReceive, r21
      /* Вернуться из функции */
      RET
     setNack:
      /* Установить линию SDA в высокое состояние */
      SDA_HIGH
      /* и обеспечить тактовый импульс */
      SCL_HIGH
      RCALL  I2cDelay
      SCL_LOW
      /* Скопировать в переменную dataReceive значение r21 */
      STS    dataReceive, r21
      /* Вернуться из функции */
      RET
.end


STM32F401
.include "I2c.h"
  .data
    ackSlave: .word 0
    ackMaster: .word 0
    dataSend: .word 0
    dataReceive: .word 0
  .text
    I2cInit:
      PUSH   {LR}
      /* Включить тактирование порта B */
      setBit (RCC + AHB1ENR), GPIOBEN
      /* Настроить оба вывода в режим Pull-down */
      setBit (GPIOB + PUPDR), (PUPDR_0 + 1)
      setBit (GPIOB + PUPDR), (PUPDR_1 + 1)
      /* Установить обе линии в высокое состояние */
      SDA_HIGH
      SCL_HIGH
      /* Вернуться из функции */
      POP    {PC}    

    I2cStart:
      PUSH   {LR}
      /* Установить обе линии в низкое состояние */
      SDA_LOW
      SCL_LOW
      BL     I2cDelay
      /* Вернуться из функции */
      POP    {PC}

    I2cRestart:
      PUSH   {LR}
      /* Установить обе линии в высокое состояние */
      SDA_HIGH
      SCL_HIGH
      BL     I2cDelay
      /* Установить обе линии в низкое состояние */
      SDA_LOW
      SCL_LOW
      BL     I2cDelay
      /* Вернуться из функции */
      POP    {PC}

    I2cStop:
      PUSH   {LR}
      /* Установить обе линии в низкое состояние */
      SCL_LOW
      SDA_LOW
      /* Установить обе линии в высокое состояние */
      SCL_HIGH
      SDA_HIGH
      BL     I2cDelay
      /* Вернуться из функции */
      POP    {PC}

    I2cDelay:
      PUSH   {LR}
      LDR    r0, =0
      LDR    r1, =I2C_DELAY_MAX
     I2cDelayLoop:
      ADD    r0, 1
      CMP    r0, r1
      BNE    I2cDelayLoop
      /* Вернуться из функции */
      POP    {PC}

    I2cSendByte:
      PUSH   {LR}
      /* Загрузить в r5 количество бит - 8 */
      LDR    r5, =8
      /* Загрузить в r7 передаваемое число из переменной dataSend */
      LDR    r6, =dataSend
      LDR    r7, [r6]
     sendBit:
      /* Проверить старший бит передаваемого числа */
      MOV    r6, r7
      AND    r6, MSBit
      /* если равен 0, перейти к метке sdaLow */
      CMP    r6, MSBit
      BNE    sdaLow
      /* в противном случае - установить линию SDA в высокое состояние */
      SDA_HIGH
      /* и перейти к метке sclTick */
      B      sclTick
     sdaLow:
      /* установить линию SDA в низкое состояние */
      SDA_LOW
     sclTick:
      /* Обеспечить тактовый импульс на линии SCL */
      SCL_HIGH
      BL     I2cDelay
      SCL_LOW
      BL     I2cDelay
      /* Сдвинуть влево число в r7 */
      LSL    r7, 1
      /* Проверить переданы ли все 8 бит */
      SUB    r5, 1
      CMP    r5, 0
      /* если нет, перейти к метке sendBit */
      BNE    sendBit
      /* Проверить ответ ведомого, для чего */
      /* освободить линию SDA */
      SDA_HIGH
      /* начать тактовый импульс */
      SCL_HIGH
      BL     I2cDelay
      /* проверить состояние линии SDA */
      LDR    r6, =(GPIOB + IDR)
      LDR    r8, [r6]
      AND    r8, (NACK << SDA)
      CMP    r8, (NACK << SDA)
      /* если ответ - NACK, перейти к метке noAck */
      BEQ    noAck
      /* в противном случае - завершить тактовый импульс */
      SCL_LOW
      /* и сохранить в переменной ackSlave значение ACK */
      LDR    r6, =ackSlave
      LDR    r7, =ACK
      STR    r7, [r6]
      /* Вернуться из функции */
      POP    {PC}
     noAck:
      /* завершить тактовый импульс */
      SCL_LOW
      /* и сохранить в переменной ackSlave значение NACK */
      LDR    r6, =ackSlave
      LDR    r7, =NACK
      STR    r7, [r6]
      /* Вернуться из функции */
      POP    {PC}

    I2cReceiveByte:
      PUSH   {LR}
      /* Загрузить в r5 количество бит - 8 */
      LDR    r5, =8
      /* Освободить линию SDA */
      SDA_HIGH
     receiveBit:
      /* Сдвинуть на 1 бит влево число в r7 для чтения следующего бита */
      LSL    r7, 1
      /* Начать тактовый импульс */
      SCL_HIGH
      BL     I2cDelay
      /* Определить значение принятого бита */
      LDR    r0, =(GPIOB + IDR)
      LDR    r1, [r0]
      AND    r1, (1 << SDA)
      CMP    r1, 0
      /* Если значение равно 0, перейти к метке tickEnd */
      BEQ    tickEnd
      /* в противном случае - установить в 1 нулевой бит r7 */
      ORR    r7, LSBit
     tickEnd:
      /* Завершить тактовый импульс */
      SCL_LOW
      BL     I2cDelay
      /* Проверить приняты ли все 8 бит */
      SUB    r5, 1
      CMP    r5, 0
      /* если нет, перейти к метке receiveBit */
      BNE    receiveBit
      /* Установить значение ответа ведомому, для чего */
      /* проверить значение переменной ackMaster */
      LDR    r0, =ackMaster
      LDR    r1, [r0]
      CMP    r1, NACK
      /* если значение ack - NACK, перейти к метке  setNack */
      BEQ    setNack
      /* в противном случае - установить линию SDA в низкое состояние */
      SDA_LOW
      /* и обеспечить тактовый импульс */
      SCL_HIGH
      BL     I2cDelay
      SCL_LOW
      /* Скопировать в переменную dataReceive значение r7 */
      LDR    r0, =dataReceive
      STR    r7, [r0]
      /* Вернуться из функции */
      POP    {PC}
     setNack:
      /* Установить линию SDA в высокое состояние */
      SDA_HIGH
      /* и обеспечить тактовый импульс */
      SCL_HIGH
      BL     I2cDelay
      SCL_LOW
      /* Скопировать в переменную dataReceive значение r7 */
      LDR    r0, =dataReceive
      STR    r7, [r0]
      /* Вернуться из функции */
      POP    {PC}
.end

nRF52832
.include "I2c.h"
  .data
    ackSlave: .word 0
    ackMaster: .word 0
    dataSend: .word 0
    dataReceive: .word 0
  .text
    I2cInit:
      PUSH  {LR}
      /* Установить обе линии в высокое состояние */
      SDA_HIGH
      SCL_HIGH
      /* Вернуться из функции */
      POP   {PC}

    I2cStart:
      PUSH  {LR}
      /* Установить обе линии в низкое состояние */
      SDA_LOW
      SCL_LOW
      BL    I2cDelay
      /* Вернуться из функции */
      POP   {PC}

    I2cRestart:
      PUSH  {LR}
      /* Установить обе линии в высокое состояние */
      SDA_HIGH
      SCL_HIGH
      BL    I2cDelay
      /* Установить обе линии в низкое состояние */
      SDA_LOW
      SCL_LOW
      BL    I2cDelay
      /* Вернуться из функции */
      POP   {PC}

    I2cStop:
      PUSH  {LR}
      /* Установить обе линии в низкое состояние */
      SCL_LOW
      SDA_LOW
      /* Установить обе линии в высокое состояние */
      SCL_HIGH
      SDA_HIGH
      BL    I2cDelay
      /* Вернуться из функции */
      POP   {PC}

    I2cDelay:
      PUSH  {LR}
      LDR   r0, =0
      LDR   r1, =I2C_DELAY_MAX
     I2cDelayLoop:
      ADD   r0, 1
      CMP   r0, r1
      BNE   I2cDelayLoop
      /* Вернуться из функции */
      POP   {PC}

    I2cSendByte:
      PUSH  {LR}
      /* Загрузить в r5 количество бит - 8 */
      LDR   r5, =8
      /* Загрузить в r7 передаваемое число из переменной dataSend */
      LDR   r6, =dataSend
      LDR   r7, [r6]
     sendBit:
      /* Проверить старший бит передаваемого числа */
      MOV   r6, r7
      AND   r6, MSBit
      /* если равен 0, перейти к метке sdaLow */
      CMP   r6, MSBit
      BNE   sdaLow
      /* в противном случае - установить линию SDA в высокое состояние */
      SDA_HIGH
      /* и перейти к метке sclTick */
      B     sclTick
     sdaLow:
      /* установить линию SDA в низкое состояние */
      SDA_LOW
     sclTick:
      /* Обеспечить тактовый импульс */
      SCL_HIGH
      BL    I2cDelay
      SCL_LOW
      BL    I2cDelay
      /* Сдвинуть влево число в r7 */
      LSL   r7, 1
      /* Проверить, переданы ли все 8 бит */
      SUB   r5, 1
      CMP   r5, 0
      /* если нет, перейти к метке sendBit */
      BNE   sendBit
      /* Проверить ответ ведомого, для чего */
      /* освободить линию SDA */
      SDA_HIGH
      /* начать тактовый импульс */
      SCL_HIGH
      BL    I2cDelay
      /* проверить состояние линии SDA */
      LDR   r6, =(GPIO + GPIO_IN)
      LDR   r8, [r6]
      AND   r8, (NACK << SDA)
      CMP   r8, (NACK << SDA)
      /* если ответ - NACK, перейти к метке noAck */
      BEQ   noAck
      /* в противном случае - завершить тактовый импульс */
      SCL_LOW
      /* и сохранить в переменной ackSlave значение ACK */
      LDR   r6, =ackSlave
      LDR   r7, =ACK
      STR   r7, [r6]
      /* Вернуться из функции */
      POP   {PC}
     noAck:
      /* завершить тактовый импульс */
      SCL_LOW
      /* и сохранить в переменной ackSlave значение NACK */
      LDR   r6, =ackSlave
      LDR   r7, =NACK
      STR   r7, [r6]
      /* Вернуться из функции */
      POP   {PC}

    I2cReceiveByte:
      PUSH  {LR}  
      /* Загрузить в r5 количество бит - 8 */
      LDR   r5, =8
      /* Освободить  линию SDA */
      SDA_HIGH
     receiveBit:
      /* Сдвинуть на 1 бит влево число в r7 для чтения следующего бита */
      LSL   r7, 1
      /* Начать тактовый импульс */
      SCL_HIGH
      BL    I2cDelay
      /* Определить значение принятого бита */
      LDR   r0, =(GPIO + GPIO_IN)
      LDR   r1, [r0]
      AND   r1, (1 << SDA)
      CMP   r1, 0
      /* Если значение равно 0, перейти к метке tickEnd */
      BEQ   tickEnd
      /* в противном случае - установить в 1 нулевой бит r7 */
      ORR   r7, LSBit
     tickEnd:
      /* Завершить тактовый импульс */
      SCL_LOW
      BL    I2cDelay
      /* Проверить приняты ли все 8 бит */
      SUB   r5, 1
      CMP   r5, 0
      /* если нет, перейти к метке receiveBit */
      BNE   receiveBit
      /* Установить значение ответа ведомому, для чего */
      /* проверить значение переменной ackMaster */
      LDR   r0, =ackMaster
      LDR   r1, [r0]
      CMP   r1, NACK
      /* если значение ack - NACK, перейти к метке setNack */
      BEQ   setNack
      /* в противном случае - установить линию SDA в низкое состояние */
      SDA_LOW
      /* и обеспечить тактовый импульс */
      SCL_HIGH
      BL    I2cDelay
      SCL_LOW
      /* Скопировать в переменную dataReceive значение r7 */
      LDR   r0, =dataReceive
      STR   r7, [r0]
      /* Вернуться из функции */
      POP   {PC}
     setNack:
      /* Установить линию SDA в высокое состояние */
      SDA_HIGH
      /* и обеспечить тактовый импульс */
      SCL_HIGH
      BL    I2cDelay
      SCL_LOW
      /* Скопировать в переменную dataReceive значение r7 */
      LDR   r0, =dataReceive
      STR   r7, [r0]
      /* Вернуться из функции */
      POP   {PC}
.end

Как следует из названий, переменные dataSend/dataReceive и ackMaster/ackSlave призваны хранить значений передаваемых/принимаемых данных и ответов ведущего/ведомого.

Комментарии к коду достаточно подробные, поэтому не вижу смысла дублировать их в тексте статьи.

Обмен данными между МК и DS1307

Подробную информацию об устройстве и нюансах работы DS1307 вы можете найти в даташите, выложенном в архив к статье.

Для МК общение с DS1307 ограничивается доступом к восьми регистрам, адреса и назначение битов которых приведены на Рисунке 6.

Рисунок 6. Регистры RTC DS1307.

Коротко о назначении битов регистров микросхемы:
1. Запись единицы в седьмой бит CH регистра с адресом 0х00 включает работу часов.

2. Остальные биты регистра с адресом 0х00, а также биты регистров с адресами 0х01— 0х06 хранят текущие значения времени и даты в формате BCD.

3. Биты 7, 4, 1 и 0 регистра с адресом 0х07 ответственны за настройку сигнала на выводе SQW микросхемы.

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

Существует два варианта доступа к регистрам микросхемы:
а) начиная с регистра, адрес которого содержится в указателе.
б) начиная с заданного регистра.

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

Рисунок 7. Обмен данными с RTC DS1307.

Рассмотрим подробнее основные моменты обмена данными.

Запись данных в регистры DS1307

1. После формирования условия Start ведущий передаёт байт содержащий:
• I2c-адрес DS1307 в старших семи битах,
• 0 в младшем бите.

2. Ведущий передаёт адрес n (от 0x00 по 0х07) регистра, начиная с которого будет производиться запись.

3. Передаётся значение:
а) регистра с адресом n – в случае записи в один регистр,
б) регистра с адресом n и следующих за ним регистров — в случае записи в несколько регистров.

4. Ведущий формирует условие Stop.

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

Чтение данных из регистров DS1307

1. После формирования условия Start ведущий передаёт байт содержащий:
• I2c-адрес DS1307 в старших семи битах,
• 0 в младшем бите.

2. Получив ответ ACK, ведущий передаёт адрес n (от 0x00 по 0х07) регистра, начиная с которого будет производиться чтение, с тем же ответом от DS1307.

3. Ведущий формирует условие Repeated Start и передаёт байт содержащий:
• I2c-адрес DS1307 в старших семи битах,
• 1 в младшем бите.
с ответом ACK от ведомого.

4. Принимается значение:
а) регистра с адресом n – в случае чтения одного регистра,
б) регистра с адресом n и следующих за ним регистров — в случае чтения нескольких регистров.

5. Приём каждого байта, кроме последнего или единственного, ведущий должен сопровождать ответом ACK, свидетельствующим об удачном обмене. В случае последнего или единственного принятого байта ответ ведущего — NACK.

6. Ведущий формирует условие Stop.

Код обмена данными МК с DS1307

В качестве примера оформим код, позволяющий записывать в DS1307 требуемое время, а также считывать текущее время и выводить его в консоль, ориентируясь на который вы сможет самостоятельно осуществить доступ и к другим регистрам RTC.

Внесём в файл ds1307.h следующую информацию, одинаковую для всех рассматриваемых нами МК: макроопределения и прототипы функций, требующиеся для реализации вышеуказанной задачи.

/* Адреса DS1307 */
/* абсолютный */
DS1307_ADDRESS       = 0x68
/* для записи */
DS1307_ADDRESS_WRITE = (DS1307_ADDRESS << 1)
/* для чтения */
DS1307_ADDRESS_READ  = ((DS1307_ADDRESS << 1) + 1)

/* Регистры и биты DS1307 */
DS1307_SECONDS       = 0x00
  CH                 = 7
  SECONDS_10         = 4
  SECONDS            = 0
DS1307_MINUTES       = 0x01
  MINUTES_10         = 4
  MINUTES            = 0
DS1307_HOURS         = 0x02
  HOURS_10           = 4
  HOURS              = 0

ACK                  = 0
NACK                 = 1

.global ds1307SetSeconds, ds1307GetSeconds, ds1307SetTime, ds1307GetTime, ds1307Init
.global seconds, minutes, hours

И, в соответствии с диаграммами из Рисунка 7, пропишем в ds1307.S упомянутые в хидер-файле функции:

AVR-8
.include "ds1307.h"
  .data
    seconds: .byte 0
    minutes: .byte 0
    hours: .byte 0
  .text
    ds1307SetSeconds:
      /* Сформировать условие Start */
      RCALL  I2cStart
      /* Передать адрес DS1307 для записи */
      LDI    r16, DS1307_ADDRESS_WRITE
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDI    r16, DS1307_SECONDS
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать значение секунд */
      LDS    r16, seconds
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Сформировать условие Stop */
      RCALL  I2cStop
      /* Вернуться из функции */
      RET
		
    ds1307GetSeconds:
      /* Загрузить NACK в переменную ackMaster */
      LDI    r16, NACK
      STS    ackMaster, r16
      /* Сформировать условие Start */
      RCALL  I2cStart
      /* Передать адрес DS1307 для записи */
      LDI    r16, DS1307_ADDRESS_WRITE
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDI    r16, DS1307_SECONDS
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Сформировать условие Repeated Start */
      RCALL  I2cRestart
      /* Передать адрес DS1307 для чтения */
      LDI    r16, DS1307_ADDRESS_READ
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Получить значение секунд и скопировать в seconds */
      RCALL  I2cReceiveByte
      LDS    r16, dataReceive
      STS    seconds, r16
      /* Сформировать условие Stop */
      RCALL  I2cStop
      /* Вернуться из функции */
      RET
			
    ds1307SetTime:
      /* Сформировать условие Start */
      RCALL  I2cStart
      /* Передать адрес DS1307 для записи */
      LDI    r16, DS1307_ADDRESS_WRITE
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDI    r16, DS1307_SECONDS
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать значение секунд */
      LDS    r16, seconds
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать значение минут */
      LDS    r16, minutes
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать значение часов */
      LDS    r16, hours
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Сформировать условие Stop */
      RCALL  I2cStop
      /* Вернуться из функции */
      RET

    ds1307GetTime:
      /* Загрузить ACK в переменную ackMaster */
      LDI    r16, ACK
      STS    ackMaster, r16
      /* Сформировать условие Start */
      RCALL  I2cStart
      /* Передать адрес DS1307 для записи */
      LDI    r16, DS1307_ADDRESS_WRITE
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDI    r16, DS1307_SECONDS
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Сформировать условие Repeated Start */
      RCALL  I2cRestart
      /* Передать адрес DS1307 для чтения */
      LDI    r16, DS1307_ADDRESS_READ
      STS    dataSend, r16
      RCALL  I2cSendByte
      /* Получить значение секунд и скопировать в seconds */
      RCALL  I2cReceiveByte
      LDS    r16, dataReceive
      STS    seconds, r16
      /* Получить значение минут и скопировать в minutes */
      RCALL  I2cReceiveByte
      LDS    r16, dataReceive
      STS    minutes, r16
      /* Загрузить NACK в переменную ackMaster */
      LDI    r16, NACK
      STS    ackMaster, r16
      /* Получить значение часов и скопировать в hours */
      RCALL  I2cReceiveByte
      LDS    r16, dataReceive
      STS    hours, r16
      /* Сформировать условие Stop */
      RCALL  I2cStop
      /* Вернуться из функции */
      RET
		
    ds1307Init:
      /* Настроить выводы I2C */
      RCALL  I2cInit
      /* Включить DS1307 */
      LDI    r16, ~(1 << CH)
      STS    seconds, r16
      RCALL  ds1307SetSeconds
      /* Вернуться из функции */
      RET
.end

Cortex M-4
.include "ds1307.h" 
  .data
    seconds: .word 0
    minutes: .word 0
    hours: .word 0
  .text
    ds1307SetSeconds:
      PUSH  {LR}
      /* Сформировать условие Start */
      BL    I2cStart
      /* Передать адрес DS1307 для записи */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_ADDRESS_WRITE
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_SECONDS
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать значение секунд */
      LDR   r0, =seconds
      LDR   r1, [r0]
      LDR   r0, =dataSend
      STR   r1, [r0]
      BL    I2cSendByte
      /* Сформировать условие Stop */
      BL    I2cStop
      /* Вернуться из функции */
      POP   {PC}

    ds1307GetSeconds:
      PUSH  {LR}
      /* Загрузить NACK в переменную ackMaster */
      LDR   r0, =ackMaster
      LDR   r1, =NACK
      STR   r1, [r0]
      /* Сформировать условие Start */
      BL    I2cStart
      /* Передать адрес DS1307 для записи */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_ADDRESS_WRITE
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_SECONDS
      STR   r1, [r0]
      BL    I2cSendByte
      /* Сформировать условие Repeated Start */
      BL    I2cRestart
      /* Передать адрес DS1307 для чтения */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_ADDRESS_READ
      STR   r1, [r0]
      BL    I2cSendByte
      /* Получить значение секунд и скопировать в seconds */
      BL    I2cReceiveByte
      LDR   r0, =dataReceive
      LDR   r1, [r0]
      LDR   r0, =seconds
      STR   r1, [r0]
      /* Сформировать условие Stop */
      BL    I2cStop
      /* Вернуться из функции */
      POP   {PC}

    ds1307SetTime:
      PUSH  {LR}
      /* Сформировать условие Start */
      BL    I2cStart
      /* Передать адрес DS1307 для записи */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_ADDRESS_WRITE
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_SECONDS
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать значение секунд */
      LDR   r0, =seconds
      LDR   r1, [r0]
      LDR   r0, =dataSend
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать значение минут */
      LDR   r0, =minutes
      LDR   r1, [r0]
      LDR   r0, =dataSend
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать значение часов */
      LDR   r0, =hours
      LDR   r1, [r0]
      LDR   r0, =dataSend
      STR   r1, [r0]
      BL    I2cSendByte
      /* Сформировать условие Stop */
      BL    I2cStop
      /* Вернуться из функции */
      POP   {PC}

    ds1307GetTime:
      PUSH  {LR}
      /* Загрузить ACK в переменную ackMaster */
      LDR   r0, =ackMaster
      LDR   r1, =ACK
      STR   r1, [r0]
      /* Сформировать условие Start */
      BL    I2cStart
      /* Передать адрес DS1307 для записи */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_ADDRESS_WRITE
      STR   r1, [r0]
      BL    I2cSendByte
      /* Передать адрес регистра DS1307_SECONDS */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_SECONDS
      STR   r1, [r0]
      BL    I2cSendByte
      /* Сформировать условие Repeated Start */
      BL    I2cRestart
      /* Передать адрес DS1307 для чтения */
      LDR   r0, =dataSend
      LDR   r1, =DS1307_ADDRESS_READ
      STR   r1, [r0]
      BL    I2cSendByte
      /* Получить значение секунд и скопировать в seconds */
      BL    I2cReceiveByte
      LDR   r0, =dataReceive
      LDR   r1, [r0]
      LDR   r0, =seconds
      STR   r1, [r0]
      /* Получить значение минут и скопировать в minutes */
      BL    I2cReceiveByte
      LDR   r0, =dataReceive
      LDR   r1, [r0]
      LDR   r0, =minutes
      STR   r1, [r0]
      /* Загрузить NACK в переменную ackMaster */
      LDR   r0, =ackMaster
      LDR   r1, =NACK
      STR   r1, [r0]
      /* Получить значение часов и скопировать в hours */
      BL    I2cReceiveByte
      LDR   r0, =dataReceive
      LDR   r1, [r0]
      LDR   r0, =hours
      STR   r1, [r0]
      /* Сформировать условие Stop */
      BL    I2cStop
      /* Вернуться из функции */
      POP   {PC}

    ds1307Init:
      PUSH  {LR}
      /* Настроить выводы I2C */
      BL    I2cInit
      /* Включить DS1307 */
      LDR   r0, =seconds
      LDR   r1, =~(1 << CH)
      STR   r1, [r0]
      BL    ds1307SetSeconds
      /* Вернуться из функции */
      POP   {PC}
.end

Тогда, код в main.S будет иметь следующий вид:

AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "ds1307.h"
.include "macro.h"

SECONDS_10_VALUE = 0
SECONDS_VALUE    = 7

MINUTES_10_VALUE = 3
MINUTES_VALUE    = 4

HOURS_10_VALUE   = 2
HOURS_VALUE      = 1

  .text
    .org Reset_vector
      RJMP   main

    .global main
    main:
      /* Указать на вершину стека */
      stackPointerInit
      /* Настроить UART */
      RCALL  uartInit
      /* Настроить ds1307 */
      RCALL  ds1307Init
      /* Установить время */
      LDI    r16, (SECONDS_10_VALUE << SECONDS_10) | (SECONDS_VALUE << SECONDS)
      STS    seconds, r16
      LDI    r16, (MINUTES_10_VALUE << MINUTES_10) | (MINUTES_VALUE << MINUTES)
      STS    minutes, r16
      LDI    r16, (HOURS_10_VALUE << HOURS_10) | (HOURS_VALUE << HOURS)
      STS    hours, r16
      RCALL  ds1307SetTime
    main_loop:
      /* Загрузить в r18 дополнение до ASCII значения */
      LDI    r18, 48
      /* Считать текущее время */
      RCALL  ds1307GetTime
      /* И вывести в PuTTy */
      /* десятки часов */
      LDS    r17, hours
      MOV    r16, r17
      ANDI   r16, (0x03 << HOURS_10)
      LSR    r16
      LSR    r16
      LSR    r16
      LSR    r16
      ADD    r16, r18
      RCALL  uartSendByte
      /* часы */
      MOV    r16, r17
      ANDI   r16, (0x0F << HOURS)
      ADD    r16, r18
      RCALL  uartSendByte
      /* разделитель */
      LDI    r16, ':'
      RCALL  uartSendByte
      /* десятки минут */
      LDS    r17, minutes
      MOV    r16, r17
      ANDI   r16, (0x07 << MINUTES_10)
      LSR    r16
      LSR    r16
      LSR    r16
      LSR    r16
      ADD    r16, r18
      RCALL  uartSendByte
      /* минуты */
      MOV    r16, r17
      ANDI   r16, (0x0F << MINUTES)
      ADD    r16, r18
      RCALL  uartSendByte
      /* разделитель */
      LDI    r16, ':'
      RCALL  uartSendByte
      /* десятки секунд */
      LDS    r17, seconds
      MOV    r16, r17
      ANDI   r16, (0x07 << SECONDS_10)
      LSR    r16
      LSR    r16
      LSR    r16
      LSR    r16
      ADD    r16, r18
      RCALL  uartSendByte
      /* секунды */
      MOV    r16, r17
      ANDI   r16, (0x0F << SECONDS)
      ADD    r16, r18
      RCALL  uartSendByte
      /* символ новой строки */
      LDI    r16, 10
      RCALL  uartSendByte
      /* символ возврата каретки */
      LDI    r16, 13
      RCALL  uartSendByte
      RCALL  delay
      RJMP   main_loop
.end

Cortex M-4
.include "stm32f401.h" /* или nrf52832.h" */
.include "ds1307.h"
.include "macro.h"

SECONDS_10_VALUE = 0
SECONDS_VALUE    = 7

MINUTES_10_VALUE = 3
MINUTES_VALUE    = 4

HOURS_10_VALUE   = 2
HOURS_VALUE      = 1

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

    .org Reset_vector
      .word main + 1

    .global main
    main:
      /* Настроить UART */
      BL    uartInit
      /* Настроить ds1307 */
      BL    ds1307Init
      /* Установить время */
      LDR   r0, =seconds
      LDR   r1, =((SECONDS_10_VALUE << SECONDS_10) | (SECONDS_VALUE << SECONDS))
      STR   r1, [r0]
      LDR   r0, =minutes
      LDR   r1, =((MINUTES_10_VALUE << MINUTES_10) | (MINUTES_VALUE << MINUTES))
      STR   r1, [r0]
      LDR   r0, =hours
      LDR   r1, =((HOURS_10_VALUE << HOURS_10) | (HOURS_VALUE << HOURS))
      STR   r1, [r0]
      BL    ds1307SetTime
    main_loop:
      /* Считать текущее время */
      BL    ds1307GetTime
      /* И вывести в PuTTy */
      /* десятки часов */
      LDR   r0, =hours
      LDR   r1, [r0]
      AND   r1, (0x03 << HOURS_10)
      LSR   r1, 4
      ADD   r1, 48
      BL    uartSendByte
      /* часы */
      LDR   r0, =hours
      LDR   r1, [r0]
      AND   r1, (0x0F << HOURS)
      ADD   r1, 48
      BL   uartSendByte
      /* разделитель */
      LDR   r1, =':'
      BL    uartSendByte
      /* десятки минут */
      LDR   r0, =minutes
      LDR   r1, [r0]
      AND   r1, (0x07 << MINUTES_10)
      LSR   r1, 4
      ADD   r1, 48
      BL    uartSendByte
      /* минуты */
      LDR   r0, =minutes
      LDR   r1, [r0]
      AND   r1, (0x0F << MINUTES)
      ADD   r1, 48
      BL    uartSendByte
      /* разделитель */
      LDR   r1, =':'
      BL    uartSendByte
      /* десятки секунд */
      LDR   r0, =seconds
      LDR   r1, [r0]
      AND   r1, (0x07 << SECONDS_10)
      LSR   r1, 4
      ADD   r1, 48
      BL    uartSendByte
      /* секунды */
      LDR   r0, =seconds
      LDR   r1, [r0]
      AND   r1, (0x0F << SECONDS)
      ADD   r1, 48
      BL    uartSendByte
      /* символ новой строки */
      LDR   r1, =10
      BL    uartSendByte
      /* символ возврата каретки */
      LDR   r1, =13
      BL    uartSendByte
      BL    delay
      B     main_loop
.end

Как видите:
а) В main:
• функция ds1307Init запускает работу часов,
• посредством макроопределений десятков и единиц произвольно выбранного как стартовое времени (21:24:07) формируются числа для дальнейшей записи через переменные seconds, minutes и hours в соответствующие регистры RTC с помощью функции ds1307SetTime.

б) В цикле main_loop с периодичностью около 1 секунды:
• текущие данные регистров DS13007 с адресами 0x00, 0x01 и 0х02 считываются функцией ds1307GetTime в вышеуказанные переменные,
• из считанных данных вычленяются десятки и единицы, к которым прибавляется число 48 с целью получить ASCII код их значений.
• полученные ASCII коды последовательно выводятся в консоль.

Далее необходимо:
1. Откомпилировать и загрузить программу в МК.

2. Соединить микроконтроллер, помимо DS1307, с компьютером через USB-UART адаптер так, как мы это делали во второй части статьи.

3. Запустить упоминавшуюся в первой части статьи программу PuTTY и в категории «Session»:
• выбрать COM-порт, к которому подключен ваш USB-UART адаптер, и скорость обмена по UART (4800 — для AVR-8, 9600 — для Cortex M-4),
• выбрать тип соединения «Serial»,
• нажатием кнопки «Save» сохранить настройки, назвав их предварительно как, например, «serial AVR» или «serial ARM»,

Рисунок 8. Настройки программы PuTTY.

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

Рисунок 9. Результат работы проекта RTC.

Код проекта RTC выложен в архив статьи.

Протокол SPI

Как и в предыдущем случае, откроем папку нового проекта Transceiver и:
• скопируем в неё шаблонные файлы, а также объектные файлы uart.o и delayAVR.o (или delayCortex.o) из архива второй части статьи,
• создадим дополнительно к шаблонным файлы SPI.S/SPI.h и nRF24.S/nRF24.h.

Управление линиями протокола SPI

На Рисунке 10 изображена схема соединений для сети SPI, состоящей из ведущего и двух ведомых.

Рисунок 10. Сеть SPI.

Пройдёмся по деталям:
1. Как говорилось выше, обмен между ведущим и ведомым происходит по двум линиям:
MOSI (Master Out Slave In) – для передачи данных от ведущего к ведомому,
MISO (Master In Slave Out) – для передачи данных от ведомого к ведущему.

2. Тактирование обмена осуществляется ведущим по линии SCK.

3. Помимо вышеуказанных, в протоколе SPI участвует активно-низкая линия SS, на которую и возложены функции:
• инициирования и завершения сеанса обмена,
• выбора ведомого.

Число выводов SS для ведущего определяется, как вы понимаете, количеством ведомых, присутствующих в сети.

4. Учитывая вышеизложенное, при инициализации протокола для МК в роли ведущего выводы MOSI, SCK и SS должны быть настроены как выходы, а MISO – как вход. Кроме того, выводы SS необходимо установить в высокое состояние.

Правила обмена данными по протоколу SPI

1. Сеанс обмена данными инициируется и завершается ведущим посредством установки линии SS в низкое и высокое состояние, соответственно. К примеру, для общения с ведомым Slave1 из Рисунка 10 ведущему необходимо:
а) установить в низкое состояние вывод SS1,
б) обменяться данными с ведомым,
в) вернуть в высокое состояние вывод SS1.

2. В отличие от I2C протокол SPI — полно-дуплексный, т.е. с каждым тактовым импульсом на один принятый от ведущего бит ведомый передаёт бит своих данных.

На Рисунке 11 представлен сеанс обмена данными, когда в ответ на принимаемое от ведущего число 160 (10100000 в двоичном представлении) ведомый Slave1 передаёт 170 (10101010 в двоичном представлении). Не смотря на то, что линии SCK и MOSI — общие для обоих ведомых, Slave2 будет игнорировать сигналы на них, поскольку вывод SS2 ведущего и соответствующая линия остаются в высоком состоянии.

Рисунок 11. Временная развёртка обмена по протоколу SPI.

Тайминг протокола SPI

Требования к временным интервалам при обмене по SPI предъявляются как самим протоколом, так и производителем конкретного устройства. Например, для трансивера nRF24L01 скорость обмена ограничена значением 10 Мбит в секунду, а величины необходимых задержек приведены на Рисунке 12.

Рисунок 12. Тайминг протокола обмена трансивера nRF24L01.

Для всех рассматриваемых нами МК достаточно обеспечить частоту тактирования на линии SCK не выше 10 МГц, что мы и реализуем посредством функции spiDelay. Все остальные задержки покрываются за счёт исполнения микроконтроллером инструкций программы.

Код протокола SPI

Учитывая, что движение данных по протоколу SPI происходит одновременно в обе стороны, объединим алгоритмы из Раздела 2, ограничившись одним РОН r0 для хранения и передаваемых и принимаемых данных.

Тогда, алгоритм действий МК в качестве ведущего при обмене с ведомым одним байтом примет следующий вид:
1. Загрузить в r0 передаваемое число.

2. Поверить старший бит числа в r0 и, если он равен 0, установить линию MOSI в низкое состояние, в противном случае — в высокое.

3. Сдвинуть число в r0 влево на один бит.

4. Начать тактовый импульс, установив линию SCK в высокое состояние.

5. Поверить состояние линии MISO и, если оно — высокое, установить в 1 младший r0.

6. Завершить тактовый импульс, установив линию SCK в низкое состояние.

7. Перейти к п. 2, пока не будет завершён обмен всеми 8 битами.

Как вы наверняка догадались, после восьмого тактового импульса в r0 окажется байт данных, принятый от ведомого.

Поскольку вариант протокола — программный, для его реализации подойдут любые выводы МК, например:
PB0 (MOSI), PB1 (MISO), PB2 (SCK) и PB4 (SS) для AVR-8.
PB1 (MOSI), PB2 (MISO), PB0 (SCK) и PB14 (SS) для STM32F401.
P0.26 (MOSI), P0.27 (MISO), P0.28 (SCK) и P0.25 (SS) для nRF52832.

Для начала оформим в SPI.h:
а) Макроопределения:
• выводов протокола,
• константы задержки для функции spiDelay,
• старшего и младшего битов байта.

б) Макросы управления линиями протокола.

в) Прототипы функций инициализации протокола, задержки и обмена байтом, а также переменной для хранения данных.

AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "macro.h"

SPI_DDR       = DDRB
SPI_PORT      = PORTB
SPI_PIN       = PINB
MOSI          = PB0
MISO          = PB1
SCK           = PB2
SS            = PB4

SPI_DELAY_MAX = 1
MSBit         = 0x80
LSBit         = 0x01

.macro SS_HIGH
  setBit    SPI_PORT, SS
.endm

.macro SS_LOW
  clearBit  SPI_PORT, SS
.endm

.macro MOSI_HIGH
  setBit    SPI_PORT, MOSI
.endm

.macro MOSI_LOW
  clearBit  SPI_PORT, MOSI
.endm

.macro SCK_HIGH
  setBit    SPI_PORT, SCK
.endm

.macro SCK_LOW
  clearBit  SPI_PORT, SCK
.endm

.global spiInit, spiDelay, spiExchageByte
.global data

STM32F401
.include "stm32f401.h"
.include "macro.h"

SS            = 14
MOSI          = 1
MISO          = 2
SCK           = 0

SPI_DELAY_MAX = 5
MSBit         = 0x80
LSBit         = 0x01

.macro SS_HIGH
  setBit    (GPIOB + ODR), SS
.endm

.macro SS_LOW
  clearBit  (GPIOB + ODR), SS
.endm

.macro MOSI_HIGH
  setBit    (GPIOB + ODR), MOSI
.endm

.macro MOSI_LOW
  clearBit  (GPIOB + ODR), MOSI
.endm

.macro SCK_HIGH
  setBit    (GPIOB + ODR), SCK
.endm

.macro SCK_LOW
  clearBit  (GPIOB + ODR), SCK
.endm

.global spiInit, spiDelay, spiExchageByte
.global data

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

SS            = 25
MOSI          = 26
MISO          = 27
SCK           = 28

SPI_DELAY_MAX = 5
MSBit         = 0x80
LSBit         = 0x01

.macro SS_HIGH
  setBit    (GPIO + GPIO_OUT), SS
.endm

.macro SS_LOW
  clearBit  (GPIO + GPIO_OUT), SS
.endm

.macro MOSI_HIGH
  setBit    (GPIO + GPIO_OUT), MOSI
.endm

.macro MOSI_LOW
  clearBit  (GPIO + GPIO_OUT), MOSI
.endm

.macro SCK_HIGH
  setBit    (GPIO + GPIO_OUT), SCK
.endm

.macro SCK_LOW
  clearBit  (GPIO + GPIO_OUT), SCK
.endm

.global spiInit, spiDelay, spiExchageByte
.global data

После чего пропишем в SPI.S вышеуказанные функции.

AVR-8
.include "SPI.h" 
  .data
    data: .byte 0
  .text
    spiInit:
      /* Настроить выводы SCK, MOSI и SS как выходы, а MISO - как вход */
      setBit   SPI_DDR, SCK
      setBit   SPI_DDR, MOSI
      setBit   SPI_DDR, SS
      clearBit SPI_DDR, MISO
      /* Установить линию SS в высокое состояние */
      SS_HIGH
      RET

    spiDelay:
      LDI      r16, SPI_DELAY_MAX
     spiDelayLoop:
      DEC      r16
      BRNE     spiDelayLoop
      /* Вернуться из функции */
      RET

    spiExchageByte:
      /* Загрузить в r20 количество бит - 8 */
      LDI      r20, 8
      /* Загрузить в r21 передаваемое число из переменной data */
      LDS      r21, data
    exchangeBit:
      /* Проверить старший бит передаваемого числа */
      LDI      r16, MSBit
      AND      r16, r21
      /* если равен 0, перейти к метке mosiLow */
      BREQ     mosiLow
      /* в противном случае - установить линию MOSI в высокое состояние */
      MOSI_HIGH
      /* сдвинуть число в r21 на один бит влево */
      LSL      r21
      /* и перейти к метке sckTick */
      RJMP     sckTick
     mosiLow:
      /* Установить линию MOSI в низкое состояние */
      MOSI_LOW
      /* и сдвинуть число в r21 на один бит влево */
      LSL      r21
     sckTick:
      /* Начать тактовый импульс */
      SCK_HIGH
      RCALL    spiDelay
      /* Поверить состояние линии MISO */
      LDS      r16, SPI_PIN
      ANDI     r16, (1 << MISO)
      /* если низкое, перйти к метке sckTickEnd */
      BREQ     sckTickEnd
      /* в противном случае - установить в 1 нулевой бит числа в r21 */
      ORI      r21, LSBit
     sckTickEnd:
      /* Завершить тактовый импульс */
      SCK_LOW
      RCALL    spiDelay
      /* Проверить, обменяны ли все 8 бит  */
      DEC      r20
      /* если нет, перейти к метке exchangeBit */
      BRNE     exchangeBit
      /* Скопировать число из r21 в переменную data */
      STS      data, r21
      /* Вернуться из функции */
      RET
.end

STM32F401
.include "SPI.h"
  .data
    data: .word 0
  .text
    spiInit:
      PUSH     {LR}
      /* Включить тактирование модуля GPIOA */
      setBit   (RCC + AHB1ENR), GPIOBEN
      /* Настроить выводы SCK, MOSI и SS как выходы, а вывод MISO - как вход */
      setBit   (GPIOB + MODER), MODER_0
      setBit   (GPIOB + MODER), MODER_1
      setBit   (GPIOB + MODER), MODER_14
      clearBit (GPIOB + MODER), MODER_2
      /* Установить линию SS в высокое состояние */
      SS_HIGH
      /* Вернуться из функции */
      POP      {PC}

    spiDelay:
      PUSH     {LR}
      LDR      r0, =0
      LDR      r1, =SPI_DELAY_MAX
     spiDelayLoop:
      ADD      r0, 1
      CMP      r0, r1
      BNE      spiDelayLoop
      /* Вернуться из функции */
      POP      {PC}

    spiExchageByte:
      PUSH     {LR}
      /* Загрузить в r5 количество бит - 8 */
      LDR      r5, =8
      /* Загрузить в r7 передаваемое число из переменной data */
      LDR      r6, =data
      LDR      r7, [r6]
     exchangeBit:
      /* Проверить старший бит передаваемого числа */
      MOV      r6, r7
      AND      r6, MSBit
      CMP      r6, MSBit
      /* если равен 0, перейти к метке mosiLow */
      BNE      mosiLow
      /* в противном случае - установить линию MOSI в высокое состояние */
      MOSI_HIGH
      /* сдвинуть число в r7 на один бит влево */
      LSL      r7, 1
      /* и перейти к метке sckTick */
      B        sckTick
     mosiLow:
      /* Установить линию MOSI в низкое состояние */
      MOSI_LOW
      /* и сдвинуть число в r7 на один бит влево */
      LSL      r7, 1
     sckTick:
      /* Начать тактовый импульс */
      SCK_HIGH
      BL       spiDelay
      /* Поверить состояние линии MISO */
      LDR      r6, =(GPIOB + IDR)
      LDR      r8, [r6]
      AND      r8, (1 << MISO)
      CMP      r8, (1 << MISO)
      /* если низкое, перйти к метке sckTickEnd */
      BNE      sckTickEnd
      /* в противном случае - установить в 1 нулевой бит числа в r7 */
      ORR      r7, LSBit
     sckTickEnd:
      /* Завершить тактовый импульс */
      SCK_LOW
      BL       spiDelay
      /* Проверить, обменяны ли все 8 бит */
      SUB      r5, 1
      CMP      r5, 0
      /* если нет, перейти к метке exchangeBit */
      BNE      exchangeBit
      /* Скопировать число из r7 в переменную data */
      LDR      r6, =data
      STR      r7, [r6]
      /* Вернуться из функции */
      POP      {PC}
.end

nRF52832
.include "SPI.h"
  .data
    data: .word 0
  .text
    spiInit:
      PUSH     {LR}
      /* Настроить выводы SCK, MOSI и SS как выходы */
      setBit   (GPIO + GPIO_DIR), SCK
      setBit   (GPIO + GPIO_DIR), MOSI
      setBit   (GPIO + GPIO_DIR), SS
      /* Настроить вывод MISO - как вход PullUp */
      clearBit (GPIO + GPIO_DIR), MISO
      LDR      r0, =(GPIO + GPIO_PIN_CNF_27)
      LDR      r1, =(1 << PIN_CNF_DIR)
      STR      r1, [r0]
      /* Установить линию SS в высокое состояние */
      SS_HIGH
      /* Вернуться из функции */
      POP      {PC}

    spiDelay:
      PUSH     {LR}
      LDR      r0, =0
      LDR      r1, =SPI_DELAY_MAX
     spiDelayLoop:
      ADD      r0, 1
      CMP      r0, r1
      BNE      spiDelayLoop
      /* Вернуться из функции */
      POP      {PC}

    spiExchageByte:
      PUSH     {LR}
      /* Загрузить в r5 количество бит - 8 */
      LDR      r5, =8
      /* Загрузить в r7 передаваемое число из переменной data */
      LDR      r6, =data
      LDR      r7, [r6]
     exchangeBit:
      /* Проверить старший бит передаваемого числа */
      MOV      r6, r7
      AND      r6, MSBit
      CMP      r6, MSBit
      /* если равен 0, перейти к метке mosiLow */
      BNE      mosiLow
      /* в противном случае - установить линию MOSI в высокое состояние */
      MOSI_HIGH
      /* сдвинуть число в r7 на один бит влево */
      LSL      r7, 1
      /* и перейти к метке sckTick */
      B        sckTick
     mosiLow:
      /* Установить линию MOSI в низкое состояние */
      MOSI_LOW
      /* и сдвинуть число в r7 на один бит влево */
      LSL      r7, 1
     sckTick:
      /* Начать тактовый импульс */
      SCK_HIGH
      BL       spiDelay
      /* Поверить состояние линии MISO */
      LDR      r6, =(GPIO + GPIO_IN)
      LDR      r8, [r6]
      AND      r8, (1 << MISO)
      CMP      r8, (1 << MISO)
      /* если низкое, перйти к метке sckTickEnd */
      BNE      sckTickEnd
      /* в противном случае - установить в 1 нулевой бит числа в r7 */
      ORR      r7, LSBit
     sckTickEnd:
      /* Завершить тактовый импульс */
      SCK_LOW
      BL       spiDelay
      /* Проверить, обменяны ли все 8 бит */
      SUB      r5, 1
      CMP      r5, 0
      /* если нет, перейти к метке exchangeBit */
      BNE      exchangeBit
      /* Скопировать число из r7 в переменную data */
      LDR      r6, =data
      STR      r7, [r6]
      /* Вернуться из функции */
      POP      {PC}
.end


Обмен данными между МК и nRF24L01

В архив статьи выложена спецификация nRF24L01, в которой подробно изложена информация об этом трансивере.

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

Но, прежде ознакомлю вас с некоторыми особенностями работы этой микросхемы:
1. После подачи питания требуется не менее 100 мс, чтобы трансивер мог воспринимать какую-либо информацию. В рамках данной статьи нет необходимости в точности указанной задержки, поэтому воспользуемся для этих целей функцией delay из объектного файла delayAVR.o или delayCortex.o.

2. При каждом обращении к нему nRF24L01 в первую очередь выдаёт на линию MISO текущее значение своего регистра STATUS.

3. Регистры трансивера при подаче питания не обнуляются, а принимают дефолтные значения. В частности для регистра STATUS такое значение — 14 (00001110 в двоичном представлении), а для регистра CONFIG, запись в который и чтение мы и реализуем, 8 (00001000 в двоичном представлении).

4. Для записи в регистр nRF24L01 необходимо прибавить к значению адреса этого регистра число 0x20, а для чтения — 0x00. К такому же результату приводит и операция ИЛИ между значением адреса и указанными числами, чем мы и воспользуемся.

5. Запись в регистры трансивера и чтение из них осуществляется посредством передачи двух байтов:
а) адреса регистра, модифицированного согласно п.4,
б) значения регистра (при записи) или числа 0xFF (при чтении).

Суммируя вышеизложенное, оформим в файле nRF24.h единые для всех рассматриваемых нами МК макроопределения и прототипы функций и переменных.

.include "SPI.h"

/*Команды nRF24L01 */
R_REGISTER    = 0x00
W_REGISTER    = 0x20
NOPP          = 0xFF

/*Регистры и биты nRF24l01 */
CONFIG        = 0x00
  MASK_RX_DR  = 6
  MASK_TX_DS  = 5
  MASK_MAX_RT = 4
  EN_CRC      = 3
  CRCO        = 2
  PWR_UP      = 1
  PRIM_RX     = 0
	
.global nRF24Init, nRF24WriteRegister, nRF24ReadRegister	
.global register, registerValue, status

А затем пропишем в nRF24.S функции инициализации протокола и обмена.

AVR-8
.include "nRF24.h" 
  .data
    register: .byte 0
      registerValue: .byte 0
      status: .byte 0
  .text
    nRF24Init:
      /* Настроить выводы SPI */
      RCALL  spiInit
      /* Обеспечить  задержку более 100 мс */
      RCALL  delay
      /* Вернуться из функции */
      RET

    nRF24WriteRegister:
      /* Начать обмен */
      SS_LOW
      /* Передать число из register + W_REGISTER */
      LDS    r16, register
      ORI    r16, W_REGISTER
      STS    data, r16
      RCALL  spiExchageByte
      /* Скопировать полученный байт в status */
      LDS    r16, data
      STS    status, r16
      /* Передать число из registerValue */
      LDS    r16, registerValue
      STS    data, r16
      RCALL  spiExchageByte
      /* Завершить обмен */
      SS_HIGH
      /* Вернуться из функции */
      RET

    nRF24ReadRegister:
      /* Начать обмен */
      SS_LOW
      /* Передать число из register + R_REGISTER */
      LDS    r16, register
      ORI    r16, R_REGISTER
      STS    data, r16
      RCALL  spiExchageByte
      /* Скопировать полученный байт в status */
      LDS    r16, data
      STS    status, r16
      /* Передать команду NOPP */
      LDI    r16, NOPP
      STS    data, r16
      RCALL  spiExchageByte
      /* Завершить обмен */
      SS_HIGH
      /* Скопировать полученный байт в registerValue */
      LDS    r16, data
      STS    registerValue, r16
      /* Вернуться из функции */
      RET
.end

Cortex M-4
.include "nRF24.h"
  .data
    register: .word 0
    registerValue: .word 0
    status: .word 0
  .text
    nRF24Init:
      PUSH  {LR}
      /* Настроить выводы SPI */
      BL    spiInit
      /* Обеспечить  задержку более 100 мс */
      BL    delay
      /* Вернуться из функции */
      POP   {PC}

    nRF24WriteRegister:
      PUSH  {LR}
      /* Начать обмен */
      SS_LOW
      /* Передать число из register + W_REGISTER */
      LDR   r0, =register
      LDR   r1, [r0]
      ORR   r1, W_REGISTER
      LDR   r0, =data
      STR   r1, [r0]
      BL    spiExchageByte
      /* Скопировать полученный байт в status */
      LDR   r0, =data
      LDR   r1, [r0]
      LDR   r0, =status
      STR   r1, [r0]
      /* Передать число из registerValue */
      LDR   r0, =registerValue
      LDR   r1, [r0]
      LDR   r0, =data
      STR   r1, [r0]
      BL    spiExchageByte
      /* Завершить обмен */
      SS_HIGH
      /* Вернуться из функции */
      POP   {PC}

    nRF24ReadRegister:
      PUSH  {LR}
      /* Начать обмен */
      SS_LOW
      /* Передать число из register + R_REGISTER */
      LDR   r0, =register
      LDR   r1, [r0]
      ORR   r1, R_REGISTER
      LDR   r0, =data
      STR   r1, [r0]
      BL    spiExchageByte
      /* Скопировать полученный байт в status */
      LDR   r0, =data
      LDR   r1, [r0]
      LDR   r0, =status
      STR   r1, [r0]
      /* Передать команду NOPP */
      LDR   r0, =data
      LDR   r1, =NOPP
      STR   r1, [r0]
      BL    spiExchageByte
      /* Завершить обмен */
      SS_HIGH
      /* Скопировать полученный байт в registerValue */
      LDR   r0, =data
      LDR   r1, [r0]
      LDR   r0, =registerValue
      STR   r1, [r0]
      /* Вернуться из функции */
      POP   {PC}
.end

Тогда, код в main.S будет выглядеть так.

AVR-8
.include "attiny85.h" /* или "atmega8.h" */
.include "nRF24.h"
  .text
    .org Reset_vector
      RJMP   main

    .global main
    main:
      stackPointerInit
      /* Настроить UART */
      RCALL  uartInit
      /* Настроить nRF24 */
      RCALL  nRF24Init
      /* Считать значение регистра CONFIG */
      LDI    r16, CONFIG
      STS    register, r16
      RCALL  nRF24ReadRegister
      /* и отправить в Terminal */
      LDS    r16, registerValue
      RCALL  uartSendByte
      /* Записать в регистр CONFIG новое значение */
      LDI    r16, CONFIG
      STS    register, r16
      LDI    r16, (1 << PWR_UP) | (1 << PRIM_RX)
      STS    registerValue, r16
      RCALL  nRF24WriteRegister
      /* Считать новое значение регистра CONFIG */
      LDI    r16, CONFIG
      STS    register, r16
      RCALL  nRF24ReadRegister
      /* и отправить в Terminal */
      LDS    r16, registerValue
      RCALL  uartSendByte
      /* Отправить в Terminal значение регистра STATUS */
      LDS    r16, status
      RCALL  uartSendByte
    main_loop:
      RJMP   main_loop
.end


Cortex M-4
.include "stm32f401.h" /* или nrf52832.h" */
.include "nRF24.h"
  .text
    .org 0
    /* Указать на вершину стека */
      .word RAMEND		
    .org Reset_vector
      .word main + 1

    .global main
    main:
      /* Настроить UART */
      BL   uartInit
      /* Настроить nRF24 */
      BL   nRF24Init
      /* Считать значение регистра CONFIG */
      LDR  r0, =register
      LDR  r1, =CONFIG
      STR  r1, [r0]
      BL   nRF24ReadRegister
      /* и отправить в Terminal */
      LDR  r0, =registerValue
      LDR  r1, [r0]
      BL   uartSendByte
      /* Записать в регистр CONFIG новое значение */
      LDR  r0, =register
      LDR  r1, =CONFIG
      STR  r1, [r0]
      LDR  r0, =registerValue
      LDR  r1, =(1 << PWR_UP) | (1 << PRIM_RX)
      STR  r1, [r0]
      BL   nRF24WriteRegister
      /* Считать новое значение регистра CONFIG */
      LDR  r0, =register
      LDR  r1, =CONFIG
      STR  r1, [r0]
      BL   nRF24ReadRegister
      /* и отправить в Terminal */
      LDR  r0, =registerValue
      LDR  r1, [r0]
      BL   uartSendByte
      /* Отправить в Terminal значение регистра STATUS */
      LDR  r0, =status
      LDR  r1, [r0]
      BL   uartSendByte
    main_loop:
      B    main_loop
.end

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

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

Рисунок 13. Модуль трансивера nRF24L01.

Обращаю ваше внимание на то, что:
• Напряжение питания трансивера — 3.3 В.
• Вывод протокола SS у данного модуля обозначен как CSN.

Соединив микроконтроллер, в который предварительно загружена наша программа, с модулем nRF24L01 с одной стороны и, через USB-UART адаптер, компьютером — с другой, вы должны получить в поле принимаемых данных программы Terminal:
1. Дефолтное и обновлённое значения регистра CONFIG – 8 и 3, соответственно.

2. Дефолтное значение регистра STATUS – 14.

Рисунок 14. Результат работы проекта Transceiver.

При необходимости, вы можете скачать код проекта из архива статьи.

Файлы и ссылки

Архив кодов к статье.zip  123,36 Kb ⇣ 15
Даташит DS1307, DS1307N, DS1307Z datasheet
Все статьи по nRF24L01

Заключение

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

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


Камрад, здесь железо для этого проекта

 

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

Нравится

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

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

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

 

 

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

 

Схема на Датагоре. Новая статья Elect_60: программа микроконтроллерного управления внешними устройствами от ПК... Многие наши коллеги желающие создать микроконтроллерное устройство, управляемое от ПК сталкиваются...
Схема на Датагоре. Новая статья Датчик угла поворота. Сельсин-датчик и приёмник на микроконтроллере.... Схема сельсин-датчика и программа микроконтроллера практически полностью взяты из журнала Радио №4...
Схема на Датагоре. Новая статья Грызем микроконтроллеры. Урок 2.... Предлагаю продолжить изучение микроконтроллеров… Второй урок будет посвящен по большей части...
Схема на Датагоре. Новая статья Предварительный усилитель Nico с низкими нелинейными искажениями и широким динамическим диапазоном... Предварительные усилители (ПУ), между источником сигнала и усилителем мощности имеют большое...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 4. Android... Вообще то я планировал рассказать сегодня про дисплей на базе ILI9481. Однако, он настолько похож...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 1. OLED дисплей 0.96" (128х64) на SSD1306... Добрый день, друзья! Эта статья открывает цикл, посвящённый средствам визуального отображения...
Схема на Датагоре. Новая статья Полупроводники. Часть первая: Электрические свойства полупроводников.... Эта статья в основном предназначена для тех, кто только-только начал первые шаги в области...
Схема на Датагоре. Новая статья Ищу схему усилителя УО1506 из установки ВРТУ 1000... Добрый день. Ищу какие либо данные или схему на усилительный блок / усилитель УО1506 из войсковой...
Схема на Датагоре. Новая статья Визуализация для микроконтроллера. Часть 2. TFT дисплей 1.8" (128х160) на ST7735... Следующий из рассматриваемых нами модулей обладает полноцветным дисплеем под управлением...
Схема на Датагоре. Новая статья Усилительные устройства: Учебник для вузов. Войшвилло Г. В.... Войшвилло Г. В. Усилительные устройства: Учебник для вузов. — 2-е изд., перераб. и доп. — М.:...
Схема на Датагоре. Новая статья Трехфазный асинхронный электродвигатель для преобразования однофазного напряжения в трехфазное: получаем 380 Вольт в гараже... У меня в гараже стоят несколько станков: токарные по дереву и металлу, фрезерный и циркулярка. Но...
Схема на Датагоре. Новая статья Усилительные устройства: Учебник для вузов. Войшвилло Г. В.... Войшвилло Г. В. Усилительные устройства: Учебник для вузов. — 2-е изд., перераб. и доп. — М.: Радио...
 

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

Ассемблер для микроконтроллера с нуля. Часть 6. Протоколы обмена данными I2C и SPI

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

 

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

galrad

Комментарий # 1 от 16-07-21, 12:53.
  • С нами с 23.08.2011
  • 106 комментариев
  • 12 публикаций
 
Здравствуйте, Ербол! Сегодня воспользовался вашими кодами для оформления протокола I2C и передачи данных в компьютер по USART. Задача стояла скорее экспериментальная, чем практическая... Заметил, что отклик происходит быстрее, чем при использовании "стандартных" библиотек. Всегда была мысль дописать эти протоколы в ассемблере, но как всегда, "руки не доходили". А а тут, вы как подарок приподнесли! Еще раз убеждаюсь в вашем высоком профессионализме! Есть ли в планах еще статьи по программированию? Что касается "начинающих", то конечно эта серия статей больше для "продвинутых". Дело в том, что очень много понятий остаются за "рамками" понимания тех, кто только начинает вникать в программирование, и это становиться неким барьером для дальнейшего продвижения. Это я увидел на примере своего племянника, который засыпал меня банальными вопросами. Простой пример: неумение пользоваться даташитами, приводит к тому, что возникает вопрос - откуда взялись ассемблерные команды? Для нас это как само собой разумеющееся, а для него неразрешимый вопрос. Еще один вопрос: зачем писать в ассемблере, когда можно писать в СИ и пользоваться готовыми библиотеками и как "привязать" ассемблерный код в программу, где все написано на СИ?. И мне пришлось долго объяснять, как можно писать как в Qt Creator и в Visual studio или AtmelStudio, что такое makefile с его командами как формировать и отлаживать программы... Наверно поэтому лучше объяснять все действия от начала и до конца, до получения готового кода вплоть до вариантов прошивки кода в микроконтроллер... чтобы было представление о всем процессе... Ну, я это так... немного отошел от темы. Вообще статьи просто шикарные! Надеюсь на продолжение... Спасибо!

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

erbol

Комментарий # 2 от 16-07-21, 14:57.
  • С нами с 11.12.2014
  • 106 комментариев
  • 17 публикаций
 
Доброго дня, Радик! 🙂
Спасибо большое за комментарий! Рад, что код статьи оказался полезным!

На подходе завершающая 7-я часть. Изначально задумывался плавный переход с ассемблера на на Си, в связи с чем и вводились некоторые элементы: хидер-файлы, упоминания об указателях и т.д. Даже была идея перейти на расширение файлов .asm, вместо .S, чтобы использовать директиву #define, а цикл основной функции в первом варианте назывался while_1 😄. Остаётся надеяться, что появятся время и возможность реализовать этот план.

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

С уважением,
Ербол Сармуханов 🙂

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