и заканчивать командой pop temp (что в больших программах может быть довольно сложно осуществить на практике). Между тем, учитывая относительную редкость обращения через UART, вероятность такого совпадения чрезвычайно мала (в данном случае ее можно оценить, как отношение среднего времени выполнения цикла анализа к промежутку между прерываниями Timer 0, что составит величину порядка 0,1 % — один шанс из 1000). И за все время работы по подобной схеме автор ни разу не получил такого сбоя. Потому я не стал усложнять программу, но, строго говоря, это неправильно, и грамотный преподаватель программирования обязательно сделал бы замечание. Чтобы соблюсти все правила, можно, например, в процедуру in_com вставить команду запрещения прерываний cli сразу по выходу из цикла опроса (перед оператором out udr, temp), а команду разрешения прерываний sei — в текст основной программы сразу после метки Gcykle. Только в этом случае следует помнить, что использование процедуры in_com (например, для отладки) всегда должно сопровождаться командой sei, иначе МК просто «сдохнет» в какой-то момент (зависнет). Кроме того, прерывания будут тогда запрещены и при выполнении команд, поступающих с компьютера, а это в общем случае не всегда желательно. Чтобы освободить себя от подобных размышлений, я и отказался от этой возможности. В крайнем случае команда с компьютера пропадет, ничего страшного — то же самое может быть при простом сбое обмена. Тем не менее помнить о том, что подобные баги возможны, следует всегда, в других случаях это может оказаться очень критичным.
В данном случае мы договариваемся, что значение $Е0 означает команду на перезапись коэффициентов в памяти, а значение $Е2 — чтение ранее записанных значений. Естественно, в первом случае программа обязана ожидать «сверху» дополнительно еще 8 байт значений, а во втором — наоборот, прочесть записанные в EEPROM коэффициенты и выдать их «наверх». Если же принятый байт не равен ни одной из этих величин, то программа спокойно возвращается по метке Gcykle и продолжает опрос бита RXC до следующего отправленного ей байта.
Для единообразия записи текста процедуры приема и отправки не вызываются напрямую, а дополнительно структурированы. Процедура приема организуется так:
рrос_Е0: ;записать коэффициенты +8 байт
rcall WriteKoeff
rjmp Gcykle
Метка proc_E0, как и метка ргос_Е2 далее, должны располагаться сразу после основного цикла Gcykle (потому что команда rjmp имеет ограниченное пространство действия, см. главу 13). Далее, где-то (в конце программы, например) записываем, наконец, собственно процедуру WriteKoeff приема коэффициентов и записи их в память. В ней мы учтем, что коэффициенты нельзя писать сразу в EEPROM, так как запись байта длится дольше, чем его прием через UART, и во избежание их потери необходим некий буфер. Но нам и не нужно его специально изобретать, т. к. коэффициенты все равно дублируются в SRAM, куда мы их первоначально и запишем. Если бы мы этого не сделали, то пришлось бы перезапускать контроллер после записи[15]. Сказанное иллюстрирует листинг 16.4.
Листинг 16.4.
WriteKoeff: ;записать коэффициенты +8 байт
cli ;запрещаем прерывания
ldi ZH,1
ldi ZL, tZH ;начальный адрес SRAM
LoopWR:
rcall in_com ;принимаем следующий байт
st Z+,temp ;сложили в SRAM
cpi ZL,pKL+1 ;до адреса pKL ровно 8 байт, см. листинг в Приложении 5
brne LoopWR
;теперь коэффициенты находятся в SRAM, складываем в EEPROM clr ZH
clr ZL ;адрес EEPROM = 0:0
ldi YH,1
ldi YL,tZH ;начальный адрес SRAM
LoopWE:
ld temp,Y+ ;забираем из SRAM
rcall WriteEEP ;переписываем в EEPROM
inc ZL ;следующий адрес
cpi ZL,8
brne LoopWE
ldi temp,$AA ;все Ok, посылаем ответ
rcall out_com
sei ;разрешаем прерывания
ret
Если все благополучно, по окончании процедуры в компьютер будет послан байт со значением $АА. Если такой байт не получен, значит, что-то, например, потерялось по дороге, или произошел еще какой-то сбой.
Процедура чтения коэффициентов вызывается так:
ргос_Е2: ;читать все коэффициенты 8 байт из EEPROM
rcall ReadKoeff
rjmp Gcykle
А собственно процедура чтения (листинг 16.5) будет гораздо короче, т. к. не требуется спешить с приемом байтов и, соответственно, обращаться к SRAM (вообще-то нам безразлично, откуда получать коэффициенты, так что будем читать из оригинала — из EEPROM).
Листинг 16.5
ReadKoeff: ;читать коэффициенты 8 байт из EEPROM
cli
clr ZH
clr ZL
LoopRE:
rcall ReadEEP
rcall out_com
inc ZL
cpi ZL,8 ;счетчик до 8
brne LoopRE
sei
ret
Разобранный нами последовательный порт UART хорош своей изумительной простотой. UART в той или иной форме содержат практически все современные контроллеры, кроме самых простых, вроде семейства Tuny. Эта простота, однако, оборачивается и некоторыми недостатками. Во-первых, UART может работать только с заранее оговоренной скоростью обмена. Это неудобно, когда вы заблаговременно не знаете характеристики линии: при соединении с компьютером на столе можно задавать скорость и 115 200, а при необходимости передачи по километровому кабелю и скорость 9600, которую мы тут выбрали, окажется чересчур высокой (подробнее об этом см. главу 18).
Если не брать во внимание способ синхронизации по отдельной линии, как в USART (лишний провод нам ни к чему), то в принципе можно придумать способ, когда устройства соединяются на самой быстрой скорости, на которой это возможно: так, например, работает модем, который со стороны компьютера есть тот же UART, отличающийся только формой передачи сигнала по линии (в нем единицы и нули передаются не уровнями напряжения, а посылками различной частоты или фазы), отчего может работать на значительно больших расстояниях. Не так уж сложно решить и задачу автоопределения на одной стороне скорости обмена, заданной на другой: нужно попробовать соединиться на различных скоростях, и когда заранее оговоренный байт (или их последовательность) будет принят верно, значит, мы достигли нужной скорости.
Можно ли, однако, решить задачу так, чтобы скорость передачи задавалась с одного конца, как в синхронном обмене, но при этом избежать лишнего провода? Оказывается, вполне возможно, если работать на небольших скоростях. Но UART имеет еще один капитальный недостаток: он предназначен только для соединения не более чем двух устройств между собой. Режим мультипроцессорного обмена USART, о котором мы упоминали, есть попытка решить эту проблему, но лучше выстроить сразу такой протокол, при котором несколько устройств могут быть соединены между собой, и без помех обмениваться данными в нужном направлении. Все последовательные интерфейсы, кроме «чистого» UART, построены именно таким образом, включая и SPI, и USB и многие другие. В том числе и протокол I2С, который мы сейчас и разберем.
Собственно термин I2С принадлежит фирме Philips, которая придумала этот интерфейс, а в описаниях AVR «местный» вариант I2С называют TWI (от two-wire, «двухпроводной»). Мы не будем вдаваться в тонкости различий этих протоколов, потому что они нам, по большому счету, безразличны — главное, что они полностью совместимы, и все внешние устройства, имеющие интерфейс I2С, будут работать с AVR. Потому во избежание путаницы мы всегда будем употреблять более распространенный термин I2С.
Этот интерфейс использует два сигнальных провода, как и UART (плюс, конечно, «землю», поэтому физически это трехпроводной интерфейс, а не двухпроводной, как его часто называют), только по одному из них (SCL) всегда передаются синхронизирующие импульсы, а собственно информация — по второму (SDA). Информация в каждый данный момент времени передается только одним устройством и только в одну сторону. С помощью I2С может быть (теоретически) соединено до 128 устройств, так, как показано на рис. 16.2. «Подтягивающие» резисторы должны иметь номинал порядка единиц или десятков килоом (чем выше скорость передачи, тем меньше). В качестве их можно использовать встроенные резисторы выходных линий портов AVR, но автор не рекомендует это делать, поскольку их номинал слишком велик для обычных скоростей передачи (см. далее).
Рис. 16.2. Соединение устройств по интерфейсу I2С (общий провод не показан)
Обратите внимание, что все устройства в этом случае обязаны иметь выход с «открытым коллектором», а привязка к шине питания обеспечивается парой внешних резисторов. Как мы знаем, выходы портов AVR построены иначе: по схеме с симметричным КМОП-выходом и третьим состоянием. Чтобы обеспечить совместимость с «открытым коллектором», здесь реализуют хитрый прием: состояние разрыва (выключенного транзистора на выходе) имитируется установкой выхода в третье состояние, т. е. фактически в режим вывода порта на вход, а включенное состояние — установкой вывода порта на выход и при этом обязательно в состояние логического нуля.
Разумеется, чтобы различить несколько устройств, каждое из них обязано иметь индивидуальный адрес. Он задается 7-битным кодом (восьмой бит байта адреса служит для других целей, как мы увидим), потому-то всего таких устройств на одной линии может быть 128. Адрес этот часто задается еще изготовителем, хотя в самом AVR он может быть, разумеется, задан программно, но для наших целей это не потребуется, и вот почему.