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

Прошлый раз, когда я описывал принципы работы I2C для новых мег, DiHalt посетовал что никто не заморачивается автоматами.
А дело тут в том, что автомат сделать достаточно просто, только он каждый раз получается «заточен» под конкретную задачу. Делать же универсал — оверкилл, хотя если вдуматься, вполне возможно.

Сперва пара соображений о использовании прерываний: да, конечно, можно повесить код автомата на прерывание, и это будет неплохо работать. Но надо ли так поступать? Контраргументы здесь простые — чтение/запись чего-то-там на шине 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 при отключенном термометре
При отключённом термометре программа получает 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
Файлы в топике: src1.zip, src2.zip, src3.zip

Комментарии (15)

RSS свернуть / развернуть
>> автомат сделать достаточно просто, только он каждый раз получается «заточен» под конкретную задачу.
И опять представлен автомат под конкретную задачу…
Я не в качестве придирки, я всегда пишу автомат под конкретную задачу, Будь то под микроконтроллер, будь то под Simatic S-300/S-400.
0
Что-то этот автомат больше обработчик мессаг винды напоминает, чем автомат.
0
  • avatar
  • Vga
  • 11 июня 2019, 03:28
Обычный автомат Мили.
0
Только как по мне — автомат спрятан в самом контроллере. Здесь только обработчик его состояний.
+1
Как будто это что-то плохое…
0
Ну не знаю, обработчик событий я бы не стал называть автоматом.
+1
Прошу, конечно, пардона, но вместо цепочки сравнений здесь напрашивается нечто другое.
Вот как было в 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 делается.
+1
Возможно и так. Но переход по таблице (в случае AVR) становится выгоден только при определённых условиях.
Одно из этих условий — последовательность и непрерывность индекса к таблице переходов. Грубо говоря, если это значения типа 45,46,47 и соответствующие им ветви.
А когда выбор идёт из значений 18, 28, 10, 50 — надо городить две таблицы — одну со значениями, вторую с адресами процедур. И тогда внезапно выясняется, что по объёму кода и по быстродействию — пяток сравнений и условных переходов становятся абсолютно вне конкуренции. Кривой и неуклюжий перебор «в лоб» рвёт как тузик грелку красивые и правильные алгоритмы.
0
Коды мастера и идут подряд. Да и полная таблица невелика…

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
0
А я о чём говорил? У вас 31 команда, то есть 62 байта памяти, у меня 14, то есть 28 байт.
0
Вообще-то 32 команды. 5 бит же в статусе. Плюс команды перехода на таблицу.

Коды мастера и идут подряд.
А это я зря что ли упомянул? Можно и выкинуть коды слейва, проверить простым сравнением. Ну и обычно такты в прерывании куда ценнее памяти.
0
Давйте посчитаем такты. В моём случае 7 сравнений, если ни одно не сработало — 14 тактов. Если сработало последнее сравнение (код 0x58), то 15 тактов. Если сработало первое сравнение — 3 такта.
Резюмируя всё это — имеем от 3х до 15 тактов на работу диспетчера, в зависимости от везения. Хочу посмотреть на ваш вариант кода.
0
Ну, скорее от 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… Ну будем считать что убедил :-[
0
Далее будет состояние 0x28, или 0x30 или 0x38. Если 0x28, то надо сделать повторный старт, а остальное это ошибки — в ответ на них надо сделать STOP
Кстати, 0x30 — это не совсем ошибка. NACK во время передачи может означать просто что данные закончились. Если слейв ждёт пакета фиксированной длины, то он вполне может выставлять NACK при получении последнего байта. Так что лучше при обработке этого состояния проверять остались ли данные для отправки, если нет — то всё ок. Если ещё остались — тогда да, сигнализировать об ошибке.
0x38 — это потеря арбитража. Правильные варианты обработки — либо установить TWSTA (тогда умная межка дождётся освобождения шины и снова сформирует старт), либо ничего не делать (если повторять попытку неохота). TWSTO ставить не нужно.
0
А дело тут в том, что автомат сделать достаточно просто, только он каждый раз получается «заточен» под конкретную задачу. Делать же универсал — оверкилл, хотя если вдуматься, вполне возможно.
Тут не соглашусь, как раз таки универсальная библиотечка рулит! В этом и есть сила I2C — всё уже готово аппаратно, немного обвязки и можно с комфортом работать с любым количеством слейвов. Даже мультимастер практически не требует никаких телодвижений.
Другое дело если писать слейв — тут действительно почти каждый случай требует индивидуального подхода.
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.