Тра-та-та-та-тата.

А дело тут в том, что автомат сделать достаточно просто, только он каждый раз получается «заточен» под конкретную задачу. Делать же универсал — оверкилл, хотя если вдуматься, вполне возможно.
Сперва пара соображений о использовании прерываний: да, конечно, можно повесить код автомата на прерывание, и это будет неплохо работать. Но надо ли так поступать? Контраргументы здесь простые — чтение/запись чего-то-там на шине I2C обычно не явлется предметом первоочерёдной важности. И если эти операции произойдут миллисекундой раньше или позже, ничего страшного не произойдёт. Ну разве что если вам надо гнать аудиопоток в ЦАП…
С другой стороны, подвешивание обработчика автомата на прерывание нагружает стек — увеличивает глубину его использования, ведь используемые регистры надо сохранять. А это тоже негативно сказывается на скорости исполнения. Одним словом — прерывания использовать можно, но я не вижу причины это делать.
В качестве примера сегодня мы напишем простенький фетчер, который раз в секунду опрашивает термометр, и кладёт значение температуры в память. Подобный подход позволяет превратить операцию измерения температуры из алгоритма «сейчас мы обсудим эти вопросы с термометром» в «актуальная температура всегда написана на стене», если вы понимаете о чём я.
Итак, начнём. Сначала краткое пояснение, как эта фигня вообще работает. Из основного цикла программы периодически поллятся две процедуры — обработчик таймера и обработчик I2C.
Всё что делает обработчик таймера — раз в секунду формирует START состояние на шине I2C.
Остальное всё делает обработчик I2C, благо последовательность состояний достаточно однозначна.
После старта, через некоторое время установится состояние 0x08. На него надо ответить записью slaw
Затем появится либо 0x18 или 0x20 или 0x38. В ответ на 0x18 надо записать указатель, а остальное это ошибки — в ответ на них надо сделать STOP
Далее будет состояние 0x28, или 0x30 или 0x38. Если 0x28, то надо сделать повторный старт, а остальное это ошибки — в ответ на них надо сделать STOP
И так далее, согласно логике работы шины. Внимательно изучив всё это, можно составить достаточно простой алгоритм:
В ответ на состояние 0x08 записать slaW
На 0x18 записать указатель
На 0x28 сделать повторный старт
На 0x10 записать slaR
На 0x40 разрешить отправку ACK
На 0x50 считать первый байт и запретить отправку ACK
На 0x58 считать второй байт и сделать STOP
На все любые другие коды просто делаем STOP
Теперь просто возьмём и закодим:
IicAuto: lds r16, TWCR
sbrs r16, TWINT ; Проверка события шины
ret ; Выход, если событие отсутствует
lds r16, TWSR ; Читаем состояние
andi r16,0b11111100 ; Маскируем биты прескалера
cpi r16, 0x08 ; Status code "A START condition has been transmitted"
breq _iiaslaw
cpi r16, 0x18 ; Status code "SLA+W has been transmitted; ACK has been received"
breq _iiaptr
cpi r16, 0x28 ; Status code "Data byte has been transmitted; ACK has been received"
breq _iiarst
cpi r16, 0x10 ; Status code "A repeated START condition has been transmitted"
breq _iiaslar
cpi r16, 0x40 ; Status code "SLA+R has been transmitted; ACK has been received"
breq _iiaReadI
cpi r16, 0x50 ; Status code "Data byte has been received; ACK has been returned"
breq _iiaRead0
cpi r16,0x58 ; Status code "Data byte has been received; NOT ACK has been returned"
breq _iiaRead2
ldi r16,(1<<TWINT|1<<TWSTO | 1<<TWEN) ; Stop CMD
_iiaWrCr: sts TWCR,r16
_iiaExit: ret
;-------------------------------------
; фрагмент автомата - отправляем sla/w
;-------------------------------------
_iiaslaw: ldi r16,0b10011010 ; slaW
_iiswrd: sts twdr,r16 ; записываем
ldi r16,(1<<TWINT|1<<TWEN)
rjmp _iiaWrCr
;-------------------------------------
; фрагмент автомата - запись указателя
;-------------------------------------
_iiaptr: ldi r16,0b0000000 ; Pointer
rjmp _iiswrd ; записываем
;-------------------------------------
; фрагмент автомата - повторный старт
;-------------------------------------
_iiarst: ldi r16,(1<<TWSTA|1<<TWINT|1<<TWEN) ; Re START
rjmp _iiaWrCr ; командуем
;-------------------------------------
; фрагмент автомата - отправка sla/r
;-------------------------------------
_iiaslar: ldi r16,0b10011011 ; slaR
rjmp _iiswrd ; записываем
;-------------------------------------
; фрагмент автомата - разрешаем подтверждение при чтении
;-------------------------------------
_iiaReadI: ldi r16,(1<<TWINT|1<<TWEN|1<<TWEA)
rjmp _iiaWrCr ; командуем
;-------------------------------------
; фрагмент автомата - чтение первого байта
;-------------------------------------
_iiaRead0: lds r16,twdr ; Вычитываем старший байт кода температуры
sts Term9800H,r16 ; и на время сохраняем его - до тех пор, пока не считаем младший байт
ldi r16,(1<<TWINT|1<<TWEN)
rjmp _iiaWrCr ; командуем
;-------------------------------------
; фрагмент автомата - чтение последнего байта
;-------------------------------------
_iiaRead2: lds var10, twdr ; Читаем младший байт температуры
ldi r16, (1<<TWINT|1<<TWSTO | 1<<TWEN) ; Stop CMD
sts TWCR,r16 ; командуем
; Теперь пересчитываем коды в градусы Кельвина
; Сущность этого магического действа описана в "сложить и приумножить"
lds var11,Term9800H ; Забираем старший байт из временного хранилища
loadvar2 25600
rcall mul16uh2
lsl var11
adc var12,r16
adc var13,r16
loadvar2 27315
add var12,var20
adc var13,var21
brcc pc+4
ldi r16,0xff
mov var12,r16
mov var13,r16
sts Temperature+0, var12
sts Temperature+1, var13
rjmp _iiaExit
А теперь слушайте внимательно — я выдам секрет, практически столь же чудовищный, как и главная циганская тайна. А именно — как конструировать автоматы. На самом деле тут нет ничего сложного — берём обычный линейный алгоритм, и находим в нём места, где он чего-то ждёт. Временного интервала, окончания конверсии, ввода с клавиатуры, ответа периферии — без разницы чего ждёт и желательно, чтобы ожидание было однотипным.
И по этому ожиданию режем алгоритм на фрагменты.
А потом формализуем критерий, по которому опять свяжем эти фрагменты воедино, по которому определим последовательность их исполнения.
В случае, который мы с вами сейчас рассматриваем — линейный алгоритм разрезан по точкам ожидания готовности I2C, а фрагменты диспетчерезуются по коду состояния интерфейса.
Файл с исходником можете скачать src1.zip. В качестве аппаратной платформы использован зомбак ардуины, мега 328. Термометр с кодом A5, адрес 0x9A.
Я опять всё усложняю.
Как вы возможно заметили (если дали себе труд заглянуть в исходник), в нашем предыдущем примере автоматически читается только температура, а загрузка конфигурации производится простым линейным алгоритмом при запуске. Сейчас я это буду исправлять.
Разделим два состояния термометра — термометр с загруженной конфигурацией, и термометр без конфигурации. И эти два состояния надо друг от друга отличать, и значит нужен признак, по которому автомат станет либо читать температуру, либо загружать конфигурацию.
Простое решение — завести переменную, в которой у нас будет храниться состояние термометра, допустим 0x00 — не сконфигурирован, 0xFF — сконфигурирован, а промежуточные значения будут счётчиком байт, переданных термометру в ходе конфигурации. Пусть эта переменная называется TermAuto.
Теперь при вызове обработчика, первое что он делает — анализирует значение TermAuto, и если там 0xFF, то отрабатываем уже описанный алгоритм, а если другое значение — отрабатывает новая ветвь алгоритма — заливка конфигурации.
Анализ TermAuto вставляем в начало диспетчера:
IicAuto: lds r16, TWCR
sbrs r16, TWINT ; Проверка события шины
ret ; Выход, если событие отсутствует
lds r16, TWSR ; Читаем состояние
andi r16,0b11111100 ; Маскируем биты прескалера
cpi r16, 0x08 ; Status code "A START condition has been transmitted"
breq _iiaslaw
lds r17, TermAuto
cpi r17, 0xFF
brne _iiaInitAuto
cpi r16, 0x18 ; Status code "SLA+W has been transmitted; ACK has been received"
breq _iiaptr
....
Добавляем ещё одну ветвь к диспетчеру, которая активируется только пока микросхема не сконфигурирована:
;-------------------------------------
; фрагмент автомата - инициализация термометра
;-------------------------------------
_iiaInitAuto: cpi r16, 0x18 ; Status code "SLA+W has been transmitted; ACK has been received"
breq _iiaptr2
cpi r16, 0x28 ; Status code "Data byte has been transmitted; ACK has been received"
breq _iiacfg
rjmp _iiafail
И пару запчастей к автомату:
;-------------------------------------
; фрагмент автомата - запись конфигурации
;-------------------------------------
_iia_cfg_first: ldi r16,0b01100000 ; Config
rjmp _iiswrd
;-------------------------------------
; фрагмент автомата - запись указателя на регистр конфигурации
;-------------------------------------
_iiaptr2: ldi r16,0b0000001 ; Pointer
rjmp _iiswrd
;-------------------------------------
; фрагмент автомата
;-------------------------------------
_iiacfg: inc r17
sts TermAuto, r17
cpi r17,1
breq _iia_cfg_first
ldi r16,0xff ; Переводим автомат в режим чтения температуры
sts TermAuto, r16
ldi r16, (1<<TWINT| 1<<TWSTO | 1<<TWEN) ; Stop CMD
sts TWCR, r16
rjmp _iiaExit
По успешному завершению заливки конфигурации устанавливаем TermAuto в 0xFF, таким образом конфигурация будет залита 1 раз после старта, а после следующего (и последующих) START произойдёт чтение температуры.
Добавляем небольшой фрагмент кода — сбрасываем TermAuto в ноль при ошибках на шине, помещаем этот код перед выдачей стопа в диспетчере:
_iiafail: ldi r17,0
sts TermAuto, r17
И в результате получаем хотплаг.

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

И продолжается чтение.

Кстати, помотрите на артефакт крупным планом — так выглядит тот самый неловкий момент, когда мастер отпустил шину, а слейв ещё не подхватил — это формирование сигнала ACK.

Исходники в файле src2.zip
Делаем красиво.
Есть маленький недостаток у получившегося автомата — при хотплаге пропускается один цикл чтения — его заменяет конфигурация. Поправим это.
Сейчас инициализация проходит по такой последовательности:
start
sla/w
cfg pointer
config
stop
Чтобы совместить загрузку конфигурации и чтение температуры, последовательность надо изменить на:
start
sla/w
cfg pointer
config
repeated start
sla/w
term pointer
repeated start
sla/r
read /ACK
read /NAK
stop
Выглядит запутанно, но надо помнить, что большинство последовательностей уже реализованы, и требуется только слегка подправить код.
Переносим анализ переменной TermAuto после поиска состояния «SLA+W has been transmitted», потму что это состояние будет одинаково отрабатываться обоими ветвями алгоритма:
lds r17, TermAuto
cpi r16, 0x18 ; Status code "SLA+W has been transmitted; ACK has been received"
breq _iiaptr
cpi r17, 0xFF
brne _iiaInitAuto
cpi r16, 0x28 ; Status code "Data byte has been transmitted; ACK has been received"
Реакцию на «SLA+W has been transmitted» несколько модифицируем — теперь в зависимости от содержимого TermAuto отправляем разные указатели.
;-------------------------------------
; фрагмент автомата - запись указателя на регистр температуры
;-------------------------------------
_iiaptr: ldi r16,0b0000000 ; Указатель на регистр температуры
cpi r17, 0
brne _iiswrd ; Записываем, если TermAuto <> 0
ldi r16,0b0000001 ; Указатель на регистр конфигурации
rjmp _iiswrd ; записываем если TermAuto == 0
Из второй ветви алгоритма удаляем _iiaptr2 вместе со ссылкой на неё, добавляем реакцию на состояние «A repeated START condition has been transmitted» и несколько меняем реакцию на «Data byte has been transmitted»
;-------------------------------------
; фрагмент автомата - инициализация термометра
;-------------------------------------
_iiaInitAuto: cpi r16, 0x28 ; Status code "Data byte has been transmitted; ACK has been received"
breq _iiacfg
cpi r16, 0x10 ; Status code "A repeated START condition has been transmitted"
breq _iiaslaw
rjmp _iiafail
;-------------------------------------
; фрагмент автомата
;-------------------------------------
_iiacfg: inc r17
sts TermAuto, r17
cpi r17,1
breq _iia_cfg_first ; При первом входе отправляем конфиг
cpi r17,2
breq _iiarst ; При втором (конфиг отправлен) - делаем повторный старт
; При третьем (после поинтера чтения) - деактивация ветви, повторный старт
ldi r17,0xFF
sts TermAuto, r17
rjmp _iiarst
Теперь смотрим, как отрабатывает автомат. При первом пуске, или после сбоя — TermAuto обнулён.
Таймер формирует старт.
После старта диспетчер вызывает _iiaslaw, который отправляет адрес с битом записи.
После отправки адреса диспетчер вызывает _iiaptr.
Поскольку TermAuto обнулён, _iiaptr отправляет указатель на регистр конфигурации.
На успешно отправленый байт диспетчер вызывает _iiacfg, потому что TermAuto <> 0xFF.
_iiacfg приращивает TermAuto (=1), и через _iia_cfg_first отправляет байт конфигурации.
На успешно отправленый байт диспетчер вызывает _iiacfg.
_iiacfg приращивает TermAuto (=2), и через _iiarst делает повторный старт.
На повторный старт через ветвь _iiaInitAuto диспетчер вызывает _iiaslaw, который отправляет адрес с битом записи.
После отправки адреса диспетчер вызывает _iiaptr. TermAuto сейчас равен 2, поэтому _iiaptr отправляет указатель на регистр температуры.
На успешно отправленый байт диспетчер вызывает _iiacfg, потому что TermAuto <> 0xFF.
_iiacfg потому что TermAuto<>1 | TermAuto<>2, записывает в TermAuto 0xFF, тем самым запрещая вход в ветвь инициализации, затем вызывает _iiarst, ещё раз делая повторный старт.
На повторный старт диспетчер вызывает _iiaslar, который записывает адрес с битом чтения, а дальше поехало по накатанной.

Страшно, да? А как вы хотели, автомат же.
Исходники в файле src3.zip
- +1
- 10 июня 2019, 17:29
- Gornist
- 3
Прошу, конечно, пардона, но вместо цепочки сравнений здесь напрашивается нечто другое.
Вот как было в 51 от филипса (собственно, автора I2C):
Собственно, из кода статуса I2C получается адрес перехода. В случае с AVR это кажись через ijmp делается.
Вот как было в 51 от филипса (собственно, автора I2C):
i2c_irq: push acc
push b
push dph
push dpl
push PSW
mov PSW,#008H; переключаю банк регистров
mov a, I2STAT
rr a
rr a
mov dptr, #I2Cvectortable
jmp @a+dptr
;......................
I2Cvectortable: ;________коды статуса, общие для режимов передачи и приёма__________
;00h->00h
ajmp i2c_error
;08h->02h - передан start
ajmp i2c_putaddress
;10h->04h - передан повторный старт
ajmp i2c_putaddress
;.......
Собственно, из кода статуса I2C получается адрес перехода. В случае с AVR это кажись через ijmp делается.
Возможно и так. Но переход по таблице (в случае AVR) становится выгоден только при определённых условиях.
Одно из этих условий — последовательность и непрерывность индекса к таблице переходов. Грубо говоря, если это значения типа 45,46,47 и соответствующие им ветви.
А когда выбор идёт из значений 18, 28, 10, 50 — надо городить две таблицы — одну со значениями, вторую с адресами процедур. И тогда внезапно выясняется, что по объёму кода и по быстродействию — пяток сравнений и условных переходов становятся абсолютно вне конкуренции. Кривой и неуклюжий перебор «в лоб» рвёт как тузик грелку красивые и правильные алгоритмы.
Одно из этих условий — последовательность и непрерывность индекса к таблице переходов. Грубо говоря, если это значения типа 45,46,47 и соответствующие им ветви.
А когда выбор идёт из значений 18, 28, 10, 50 — надо городить две таблицы — одну со значениями, вторую с адресами процедур. И тогда внезапно выясняется, что по объёму кода и по быстродействию — пяток сравнений и условных переходов становятся абсолютно вне конкуренции. Кривой и неуклюжий перебор «в лоб» рвёт как тузик грелку красивые и правильные алгоритмы.
Коды мастера и идут подряд. Да и полная таблица невелика…
twi_jump:
rjmp twi_error ; 0x00 TW_BUS_ERROR
rjmp twi_start_done ; 0x08 TW_START
rjmp twi_start_done ; 0x10 TW_REP_START
rjmp twi_xmit_byte ; 0x18 TW_MT_SLA_ACK
rjmp twi_no_reply ; 0x20 TW_MT_SLA_NACK
rjmp twi_xmit_byte ; 0x28 TW_MT_DATA_ACK
rjmp twi_xmit_nak ; 0x30 TW_MT_DATA_NACK
rjmp twi_arb_lost ; 0x38 TW_MT_ARB_LOST, TW_MR_ARB_LOST
rjmp twi_recv_start ; 0x40 TW_MR_SLA_ACK
rjmp twi_no_reply ; 0x48 TW_MR_SLA_NACK
rjmp twi_recv_byte ; 0x50 TW_MR_DATA_ACK
rjmp twi_recv_last_byte ; 0x58 TW_MR_DATA_NACK
rjmp twi_error ; 0x60 TW_SR_SLA_ACK
rjmp twi_error ; 0x68 TW_SR_ARB_LOST_SLA_ACK
rjmp twi_error ; 0x70 TW_SR_GCALL_ACK
rjmp twi_error ; 0x78 TW_SR_ARB_LOST_GCALL_ACK
rjmp twi_error ; 0x80 TW_SR_DATA_ACK
rjmp twi_error ; 0x88 TW_SR_DATA_NACK
rjmp twi_error ; 0x90 TW_SR_GCALL_DATA_ACK
rjmp twi_error ; 0x98 TW_SR_GCALL_DATA_NACK
rjmp twi_error ; 0xA0 TW_SR_STOP
rjmp twi_error ; 0xA8 TW_ST_SLA_ACK
rjmp twi_error ; 0xB0 TW_ST_ARB_LOST_SLA_ACK
rjmp twi_error ; 0xB8 TW_ST_DATA_ACK
rjmp twi_error ; 0xC0 TW_ST_DATA_NACK
rjmp twi_error ; 0xC8 TW_ST_LAST_DATA
rjmp twi_error ; 0xD0
rjmp twi_error ; 0xD8
rjmp twi_error ; 0xE0
rjmp twi_error ; 0xE8
rjmp twi_error ; 0xF0
rjmp twi_error ; 0xF8 TW_NO_INFO
Вообще-то 32 команды. 5 бит же в статусе. Плюс команды перехода на таблицу.
Коды мастера и идут подряд.А это я зря что ли упомянул? Можно и выкинуть коды слейва, проверить простым сравнением. Ну и обычно такты в прерывании куда ценнее памяти.
Давйте посчитаем такты. В моём случае 7 сравнений, если ни одно не сработало — 14 тактов. Если сработало последнее сравнение (код 0x58), то 15 тактов. Если сработало первое сравнение — 3 такта.
Резюмируя всё это — имеем от 3х до 15 тактов на работу диспетчера, в зависимости от везения. Хочу посмотреть на ваш вариант кода.
Резюмируя всё это — имеем от 3х до 15 тактов на работу диспетчера, в зависимости от везения. Хочу посмотреть на ваш вариант кода.
Ну, скорее от 6 до 18 — ещё статус загрузить надо… Ну и я обычно ещё 0x38 на всякий случай обрабатываю, так уже от 6 до 20 выйдет, т.е. 13 в среднем.
; Load event code
lds R16,_SFR_MEM_ADDR(TWSR) ; 2
lsr R16 ; 1
lsr R16 ; 1
lsr R16 ; 1
; Jump to table
ldi ZL,lo8(pm(twi_jump)) ; 1
ldi ZH,hi8(pm(twi_jump)) ; 1
add ZL,R16 ; 1
adc ZH,R17 ; =0 ; 1
ijmp ; 2
Хм, 11 + джамп по таблице = 13… Ну будем считать что убедил :-[
Далее будет состояние 0x28, или 0x30 или 0x38. Если 0x28, то надо сделать повторный старт, а остальное это ошибки — в ответ на них надо сделать STOPКстати, 0x30 — это не совсем ошибка. NACK во время передачи может означать просто что данные закончились. Если слейв ждёт пакета фиксированной длины, то он вполне может выставлять NACK при получении последнего байта. Так что лучше при обработке этого состояния проверять остались ли данные для отправки, если нет — то всё ок. Если ещё остались — тогда да, сигнализировать об ошибке.
0x38 — это потеря арбитража. Правильные варианты обработки — либо установить TWSTA (тогда умная межка дождётся освобождения шины и снова сформирует старт), либо ничего не делать (если повторять попытку неохота). TWSTO ставить не нужно.
А дело тут в том, что автомат сделать достаточно просто, только он каждый раз получается «заточен» под конкретную задачу. Делать же универсал — оверкилл, хотя если вдуматься, вполне возможно.Тут не соглашусь, как раз таки универсальная библиотечка рулит! В этом и есть сила I2C — всё уже готово аппаратно, немного обвязки и можно с комфортом работать с любым количеством слейвов. Даже мультимастер практически не требует никаких телодвижений.
Другое дело если писать слейв — тут действительно почти каждый случай требует индивидуального подхода.
Комментарии (15)
RSS свернуть / развернуть