В проекте из предыдущей части нашей ассемблерной эпопеи мы подключали к микроконтроллеру светодиод и потенциометр, которые не поддерживают обратной связи с МК и не являются цифровыми, т. е. не оперируют численными значениями.
То же самое можно сказать и о некоторых других внешних устройствах: кнопке, терморезисторе, реле и т. д.
Без сомнения, все указанные элементы очень важны для создания того или иного девайса, но обойтись только ими вряд ли удастся. Рано или поздно вам придётся использовать более сложные устройства с собственными контроллерами на борту, которые уже могут общаться с микроконтроллером в двустороннем порядке, причём в цифровом формате, могут преобразовывать данные (уровень освещённости, угол поворота ротора серводвигателя) в численное значение для передачи его в МК либо, наоборот, число, полученное из МК — в текущее значение какого-либо своего параметра (частота генерируемого сигнала, порог срабатывания сенсора).
Кроме того, нередко возникает необходимость обмена информацией с другим девайсом на базе МК, смартфоном или компьютером.
Во всех этих случаях движение данных между сторонами обмена регламентируется набором правил, именуемых протоколом, с двумя из которых — I2C и SPI — мы сегодня и познакомимся.
Содержание статьи / Table Of Contents
↑ Введение
Обмен восьми-битным числом между двумя устройствами может быть:а) Параллельным, когда все биты числа передаются/принимаются по 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 – в двоичном представлении).
Рисунок 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 ⇣ 70• Даташит DS1307, DS1307N, DS1307Z datasheet
• Все статьи по nRF24L01
↑ Заключение
На этом этап нашего пути под названием «Программирование» завершён.Естественно, в рамках одной статьи трудно отразить все нюансы, связанные с этим аспектом. Тем не менее, я постарался осветить наиболее важные моменты с подробностью, достаточной для того, чтобы вы уже сами смогли восполнить пробелы.
Продолжение следует.
Спасибо за внимание!
Камрад, рассмотри датагорские рекомендации
🌼 Полезные и проверенные железяки, можно брать
Опробовано в лаборатории редакции или читателями.