Продолжая цикл публикаций по микроконтроллерам на ядре Cortex-M0 компании Megawin (см. предыдущие статьи 1, 2, 3, 4, 5, 6 и 7), сегодня рассмотрим модуль интерфейса SPI в роли ведущего и ведомого, а также:
Ethernet-контроллер ENC28J60 в качестве внешнего slave-устройства,
контроллер DMA для высокоскоростного обмена данными по шине SPI,
контроллер прерываний EXIC для генерации прерывания по сигналу с внешнего устройства,
модуль GPL для аппаратного вычисления CRC32 кадров Ethernet.
Отметим, что ранее рассмотренные модули URT0, URT1 и URT2 также могут работать в качестве портов SPI в роли ведущего и ведомого, но управляются они иначе.
Функциональные возможности модуля SPI
Микроконтроллеры серии MG32F02 включают один модуль интерфейса SPI0, имеющий следующие функциональные особенности:
работа в режиме ведущего (master) или ведомого (slave);
режимы передачи данных: полный дуплекс, полудуплекс, симплекс;
программное или аппаратное управление сигналом NSS (Chip Select);
работа без сигнала NSS;
поддержка режима multi-master;
частота шины до 24 МГц в режиме ведущего, до 16 МГц в режиме ведомого;
передача данных по обоим фронтам сигнала SCK (удвоенная скорость передачи);
выбор полярности и фазы сигнала SCK;
выбор порядка бит в кадре: MSB или LSB;
настраиваемый размер одного кадра от 4 до 32 бит;
буферы приема и передачи размером 4 байта;
конфигурация линий передачи: стандартный дуплексный режим SPI, двунаправленный одно-, двух-, четырех- и восьмиразрядный режим;
поддержка восьми типов микросхем flash-памяти с интерфейсом SPI;
поддержка DMA;
работа в режимах питания ON и SLEEP.
Восьмиразрядный режим передачи не реализован в МК MG32F02A032. Функциональная схема модуля SPI0 приведена на рисунке.
Модуль включает следующие основные узлы:
схему тактирования,
схему управления выводами «IO Control»,
блок формирования сигнала SCK шины SPI «Clock Control»,
блок управления буферами передачи и приема «TX/RX Control»,
блок формирования сигнала выбора кристалла «NSS Timing Control»,
блок управления режимами «Master/Slave Control»,
регистр для приема данных «Receive Data Register»,
регистр для отправки данных «Transmit Data Register»,
блок детектирования ошибок «Error Detector»,
блок формирования сигнала прерывания «Interrupt Control».
Тактирование
Общее тактирование модуля осуществляется сигналом CK_SPI0_PR (Process Clock) от подсистемы тактирования МК. Источник сигнала определяется битом CSC_CKS1.CSC_SPI0_CKS
из сигналов CK_APB (0) или CK_AHB (1). Для формирования основного синхронизирующего сигнала шины SPI используется внутренний сигнал CK_SPI0_INT, формируемый из сигнала CK_SPI0.
Источником сигнала CK_SPI0 в зависимости от значения поля SPI0_CLK.SPI0_CK_SEL
может быть сигнал CK_SPI0_PR или выходной сигнал таймера TM00_TRGO. Сигнал CK_SPI0 поступает на первый делитель DIV с коэффициентом 1, 2, 4 или 8, выбираемым в поле SPI0_CLK.SPI0_CK_PDIV
. Далее сигнал поступает на предделитель с выбираемым в трехразрядном поле SPI0_CLK.SPI0_CK_PSC
коэффициентом деления из диапазона 1-8. Итоговый сигнал CK_SPI0_INT получается на выходе второго делителя DIV с коэффициентом 2, 4, 8 или 16, выбираемым в поле SPI0_CLK.SPI0_CK_DIV
. Частота FINT сигнала CK_SPI0_INT определяется выражением
FINT = F(CK_SPI0) / [ PDIV·(PSC + 1)·DIV ],
где F(CK_SPI0) — частота сигнала CK_SPI0, а PDIV, PSC и DIV — значения соответствующих полей регистра SPI0_CLK
.
Частота FSCK синхронизирующего сигнала SCK шины SPI соответствует частоте FINT и не должна превышать половины F(CK_SPI0) в режиме ведущего и 1/4 F(CK_SPI0) в режиме ведомого. Максимальное значение FSCK в режиме ведущего составляет 24 МГц для МК MG32F02A032 и 22 МГц для остальных МК. Максимальное значение FSCK в режиме ведомого составляет 16 МГц для всех актуальных МК серии.
Режимы работы модуля SPI
Основные параметры конфигурации модуля SPI0 собраны в регистре SPI0_CR0
. Модуль SPI0 включается установкой бита SPI0_EN
. Режим синхронизации по сигналу SCK настраивается следующими разрядами:
SPI0_CPOL
— исходное состояние сигнала SCK: 0 - низкий уровень, 1 - высокий уровень;SPI0_CPHA
— фаза тактового сигнала SCK: 0 - передний фронт (нарастающий при CPOL=0, спадающий при CPOL=1), 1 - задний фронт (нарастающий при CPOL=1, спадающий при CPOL=0).
По-умолчанию, данные в кадре передаются начиная со старшего бита (MSB). При установке бита SPI0_LSB_EN
данные передаются начиная с младшего бита (LSB).
Общий режим работы в роли ведущего или ведомого определяется полем SPI0_MDS
:
0 — модуль работает в роли ведомого устройства (Slave),
1 — модуль работает в роли ведущего устройства (Master).
Дополнительные параметры конфигурации собраны в регистре SPI0_CR2
, рассмотрим некоторые из них. При установке бита SPI0_DTR_EN
в режиме ведущего включается режим удвоенной скорости передачи по обоим фронтам синхросигнала SCK. При установке бита SPI0_BDIR_OE
включается двунаправленный режим обмена данными по одной линии интерфейса. Поле SPI0_DAT_LINE
определяет число линий передачи данных и их режим работы:
0 — стандартный дуплексный режим SPI с линиями MOSI и MISO;
1 — полудуплексный режим с одной двунаправленной линией MOSI;
2 — полудуплексный режим с двумя двунаправленными линиями D0(MOSI) и D1(MISO);
3 — полудуплексный режим с четырьмя двунаправленными линиями D0-D3;
4 — полудуплексный режим с четырьмя двунаправленными линиями D0-D3, дублированными на линии D4-D7 (режим отсутствует в МК MG32F02A032);
5 — полудуплексный режим с восемью двунаправленными линиями D0-D7 (режим отсутствует в МК MG32F02A032).
В 5-битовом поле SPI0_DSIZE
задается размер одного кадра для передачи в битах. Поле целиком располагается в 2-м байте регистра SPI0_CR2
, что удобно для быстрого изменения в процессе передачи. Бит SPI0_TXUPD_EN
разрешает прямое обновление промежуточного буфера по данным регистра SPI0_TDAT
(или SPI0_TDAT3
) в режиме ведомого.
Буферы приема и передачи
На следующем рисунке показан общий алгоритм работы буферов приема и передачи данных модуля SPI.
Принимаемые данные из входного регистра сдвига (RX Shift Buffer) передаются в промежуточный 32-разрядный буфер приема (RX Shadow Buffer). Число полученных байт обновляется в поле SPI0_STA.SPI0_RX_LVL
и всегда доступно по чтению.
В режиме ведущего после заполнения промежуточного буфера данные копируются в 32-разрядный регистр SPI0_RDAT
, после чего активируется флаг RXF, сигнализируя о их готовности к считыванию. В режиме ведомого в поле SPI0_CR2.SPI0_RX_TH
задается пороговое число байт для приема. При накоплении указанного числа байт в промежуточном буфере они копируются в регистр SPI0_RDAT
, после чего активируется флаг RXF. Число скопированных байт отображается в поле SPI0_STA.SPI0_RNUM
.
После считывания данных из регистра SPI0_RDAT
флаг RXF автоматически сбрасывается. Если данные из регистра SPI0_RDAT
не были востребованы программой к моменту следующего копирования из промежуточного буфера, генерируется событие переполнения буфера приема Receive overrun и активируется флаг ROVRF.
Данные для передачи записываются программой в регистр SPI0_TDAT
. Разрядность операции записи в регистр должна соответствовать объему передаваемых данных: для записи одного байта нужно использовать байтовую пересылку (STRB
) в младший байт регистра, для двух байт — двухбайтовую (STRH
), для 4 байт — пересылку слова (STR
). Для передачи 3 байт следует использовать отдельный регистр SPI0_TDAT3
.
После записи данных программой они копируются в промежуточный 32-битный буфер передачи (TX Shadow Buffer). После этого активируется флаг TXF, сигнализируя о готовности принять следующие данные для отправки через регистр SPI0_TDAT
или SPI0_TDAT3
. При очередной записи в эти регистры флаг TXF автоматически сбрасывается. Каждый байт из промежуточного буфера копируется в выходной регистр сдвига (TX Shift Buffer) и под действием синхроимпульсов сигнала SCK передается в линию интерфейса. Число байт, которые еще не переданы, отображается в поле SPI0_STA.SPI0_TX_LVL
.
После фактической отправки всех данных из промежуточного буфера генерируется событие Transmission complete и активируется флаг TCF. Это событие можно использовать, например, при полудуплексном обмене.
При установке бита SPI0_CR1.SPI0_RDAT_CLR
происходит "сброс" буфера приема: сбрасывается промежуточный буфер, устанавливается в 0 значение SPI0_RX_LVL
и сбрасывается флаг RXF.
При установке бита SPI0_CR1.SPI0_TDAT_CLR
происходит "сброс" буфера передачи: сбрасывается промежуточный буфер, устанавливается в 0 значение SPI0_TX_LVL
и сбрасывается флаг TXF.
В режиме ведомого возможно принудительное копирование текущих данных из промежуточного буфера в регистр SPI0_CR2.SPI0_RDAT
при установке бита SPI0_CR1.SPI0_RSB_TRG
. Бит сбрасывается аппаратно после копирования.
Режим ведущего (Master)
Настройка выводов
В обычном дуплексном режиме SPI вывод МК для сигнала MOSI может быть настроен как выход с открытым стоком (поле PxCRn.IOMn
=1) или как двухтактный выход (push-pull) (поле PxCRn.IOMn
=2). Однако, режим работы вывода будет также определяться значением поля SPI0_CR0.SPI0_DOUT_MDS
:
0 (Disabled) — в исходном состоянии и при передаче логической "1" вывод будет находиться в высокоомном состоянии независимо от значения
PxCRn.IOMn
, для работы потребуется резистор подтяжки;1 (Enabled) — модуль будет формировать полноценный уровень логической "1", но только при
PxCRn.IOMn
=2 (push-pull).
Настройка вывода МК для сигнала SCK (значение поля PxCRn.IOMn
1 или 2) применяется как обычно.
Модуль поддерживает аппаратное (автоматическое) или программное управление сигналом выбора кристалла NSSO. Для использования сигнала необходимо установить бит SPI0_CR0.SPI0_NSSO_EN
. В режиме аппаратного управления (SPI0_CR0.SPI0_NSS_PEN
=1) сигнал NSSO активируется на всё время передачи одного кадра указанного размера (см. поле SPI0_CR2.SPI0_DSIZE
). В режиме программного управления (SPI0_CR0.SPI0_NSS_PEN
=0, SPI0_CR0.SPI0_NSS_SWEN
=1) сигнал управляется изменением бита SPI0_CR2.SPI0_NSS_SWO
. Бит SPI0_CR0.SPI0_NSSO_INV
отвечает за включение инверсии сигнала NSSO. Длительность неактивного состояния сигнала NSSO определяется в поле SPI0_CR1.SPI0_NSS_IDT
: 1 такт (0) или 2 такта (1).
Передача и прием
В режиме ведущего устройства передача данных по линии MOSI начинается сразу после того, как программа записала данные в регистр SPI0_TDAT
или SPI0_TDAT3
, при этом начинают формироваться импульсы сигнала SCK согласно настройкам SPI0_CPOL
и SPI0_CPHA
. Передачу данных необходимо производить и для приема данных, который происходит по линии MISO одновременно с передачей под действием тех же импульсов сигнала SCK. Таким образом, чтобы прочитать кадр заданной длины (например, 8 бит) ведущему нужно сначала отправить кадр этой длины и после этого по активации флага RXF прочитать данные из регистра SPI0_RDAT
. Этот же флаг можно использовать наравне с TCF для определения завершения фактической передачи кадра. В режиме DMA синхроимпульсы формируются автоматически при приеме заданного объема данных.
Режим ведомого (Slave)
Настройка выводов
В обычном дуплексном режиме SPI для ведомого устройства вывод MISO может быть настроен как выход с открытым стоком (поле PxCRn.IOMn
=1) или как двухтактный выход (push-pull) (поле PxCRn.IOMn
=2) в зависимости от наличия резистора подтяжки. Остальные выводы MOSI, SCK и NSS (NSSI) должны быть настроены как цифровые входы.
Модуль поддерживает аппаратное управление с помощью сигнала выбора кристалла SPI0_NSS_IN, в качестве источника которого можно выбрать вывод NSS (SPI0_NSSI_SEL
=0) или NSSI (SPI0_NSSI_SEL
=1). Для использования сигнала управления необходимо установить бит SPI0_CR0.SPI0_NSSI_EN
. Бит SPI0_CR0.SPI0_NSSI_INV
отвечает за включение инверсии сигналов с выводов NSS и NSSI.
В модуле возможно и программное управление при установке бита SPI0_CR0.SPI0_NSSI_SWEN
. В этом случае сигнал SPI0_NSS_IN управляется изменением бита SPI0_CR2.SPI0_NSS_SWI
. Если программное управление отключено (SPI0_NSSI_SWEN
=0), этот бит можно использовать в качестве бита статуса внешнего сигнала SPI0_NSS_IN.
Передача и прием
В режиме ведомого программе необходимо реагировать на флаг RXF и без задержки считывать данные из регистра SPI0_RDAT
. Если после приема требуется отправка данных ведущему, их нужно как можно раньше поместить в буфер передачи. Однако первый байт данных копируется в промежуточный буфер на отправку еще в момент приема последнего бита (такта сигнала SCK) предыдущего кадра, т.е. фактически это может произойти еще до получения команды, на которую ведомому нужно ответить и поместить необходимые данные на отправку.
Решает эту проблему включение режима прямого обновления буфера передачи путем установки бита SPI0_CR2.SPI0_TXUPD_EN
. В этом режиме при записи в регистр SPI0_TDAT
или SPI0_TDAT3
данные также копируются в промежуточный буфер с начальной позиции.
Если к моменту отправки очередных данных они еще не были доступны в промежуточном буфере, т.е. не были своевременно записаны в регистр SPI0_TDAT
или SPI0_TDAT3
, генерируется событие Slave mode transmit underrun и активируется флаг TUDRF. При этом на линии MISO формируется высокий уровень и под действием синхроимпульсов SCK ведущий начинает считывать единицы и получать байты 0xFF (если был настроен 8-битный режим).
События и прерывания
Флаги событий модуля SPI0 собраны в регистре SPI0_STA
, а биты разрешения прерываний — в регистре SPI0_INT
. Перечень событий и соответствующих флагов прерываний приведен в следующей таблице.
Разряд |
Флаг события |
Бит прерывания |
Название события |
Описание |
---|---|---|---|---|
0 |
BUSYF |
- |
Data Transfer Busy Flag |
Модуль занят передачей данных |
3 |
IDLF |
IDL_IE |
Slave mode NSS idle detect |
Состояние Idle в режиме Slave |
4 |
TCF |
TC_IE |
Transmission complete |
Передача данных из регистра сдвига завершена |
5 |
RXDF |
- |
Received data byte number is different |
Число принятых байт в регистре |
6 |
RXF |
RX_IE |
Receive data register not empty |
Принятые данные готовы к чтению |
7 |
TXF |
TX_IE |
Transmit data register empty |
Буфер передачи готов к записи новых данных |
8 |
MODF |
MODF_IE |
SPI mode detect fault |
Активация сигнала NSS со стороны внешнего устройства в режиме Master |
9 |
WEF |
WE_IE |
Slave mode write error |
В режиме Slave ведущий прекратил чтение деактивацией сигнала NSS, не завершив операцию |
10 |
ROVRF |
ROVR_IE |
Receive overrun |
Переполнение буфера приема |
11 |
TUDRF |
TUDR_IE |
Slave mode transmit underrun |
Буфер передачи не содержит данных в режиме Slave |
31 |
IDL_STA |
- |
Idle state detect status for Slave with NSS mode |
Обнаружено состояние Idle в режиме Slave с включенным управлением от сигнала NSS |
Кроме флагов событий, регистр SPI0_STA
содержит три 3-разрядных поля:
SPI0_RX_LVL
— число несчитанных байт из буфера приема (только чтение);SPI0_TX_LVL
— число байт в буфере, ожидающих отправки (только чтение);SPI0_RNUM
— число принятых байт, переданных из промежуточного буфера в регистрSPI0_RDAT
(чтение и запись).
Схема формирования сигнала прерывание INT_SPI0 модуля SPI0 приведена на следующем рисунке.
Для включения генерации прерывания необходимо:
Выбрать событие (или события) в регистре
SPI0_INT
.Разрешить прерывание самого модуля установкой бита
SPI0_INT.SPI0_IEA
.Разрешить прерывание IRQ#24 от модуля SPI0 в контроллере прерываний NVIC в регистре
CPU_ISER
.
Взаимодействие с контроллером DMA
Общие сведения по контроллеру DMA
Контроллер DMA имеется во всех МК серии, но включает разное число каналов: в МК MG32F02A032 — один канал, в остальных МК — пять каналов. Модуль DMA включает следующие общие регистры:
Регистр |
Назначение |
---|---|
|
Флаги состояния всех каналов, по 4 флага на канал, расположены начиная с 0-го разряда |
|
Регистр включает единственный используемый бит |
|
Регистр включает общие поля настройки, а также дублирует биты включения каналов из регистров |
Каждый канал включает следующие восемь регистров (n
— номер канала 0-4):
Регистр |
Назначение |
---|---|
|
Основные поля управления каналом, биты разрешения прерывания, флаги состояния |
|
Дополнительные поля настройки канала, включая коды источника и приемника данных |
|
Объем передаваемых данных, начальное значение (байт) (биты 0-16) |
|
Объем оставшихся для передачи данных, текущее значение (байт) (биты 0-16) |
|
Адреса начала области памяти источника |
|
Текущий адрес передачи в источнике |
|
Адреса начала области памяти приемника |
|
Текущий адрес передачи в приемнике |
В следующей таблице представлены возможные источники и приемники данных:
Код для полей |
Источник |
Приемник |
||
---|---|---|---|---|
MG32F02A032 |
MG32F02x064 MG32F02x128 |
MG32F02A032 |
MG32F02x064 MG32F02x128 |
|
0 |
MEM_Read |
MEM_Read |
MEM_Write |
MEM_Write |
1 |
ADC0_IN |
ADC0_IN |
- |
DAC_OUT |
2 |
I2C0_RX |
I2C0_RX |
I2C0_TX |
I2C0_TX |
3 |
- |
I2C1_RX |
- |
I2C1_TX |
4 |
URT0_RX |
URT0_RX |
URT0_TX |
URT0_TX |
5 |
URT1_RX |
URT1_RX |
URT1_TX |
URT1_TX |
6 |
- |
URT2_RX |
- |
URT2_TX |
7 |
- |
- |
- |
- |
8 |
SPI0_RX |
SPI0_RX |
SPI0_TX |
SPI0_TX |
9 |
- |
- |
- |
- |
10 |
- |
USB_RX |
- |
USB_TX |
11 |
- |
- |
GPL_Write |
GPL_Write |
12 |
- |
- |
TM36_CC0B |
TM36_CC0B |
13 |
- |
- |
TM36_CC1B |
TM36_CC1B |
14 |
- |
- |
TM36_CC2B |
TM36_CC2B |
15 |
TM36_IC3 |
TM36_IC3 |
- |
- |
При настройке заданного канала n
нужно выполнить по крайней мере следующее:
включить канал установкой бита
DMA_CHnA.DMA_CHn_EN
;установить код источника данных в поле
DMA_CHnB.DMA_CHn_SRC
;если источник — память (MEM_Read), в регистре
DMA_CHnSSA
установить адрес начала области памяти источника;установить код приемника данных в поле
DMA_CHnB.DMA_CHn_DET
;если приемник — память (MEM_Write), в регистре
DMA_CHnDSA
установить адрес начала области памяти приемника;установить объем пересылаемых данных (байт) в регистре
DMA_CHnNUM
;установить размер единицы передаваемой информации (1, 2 или 4 байта) в поле
DMA_CHnA.DMA_CH0_BSIZE
.
Запуск пересылки данных по каналу DMA начинается сразу после установки бита DMA_CHnA.DMA_CHn_REQ
.
В модуле DMA поддерживаются следующие события и флаги (см. регистр DMA_CHnA
):
Флаг события |
Бит прерывания |
Название события |
Описание |
---|---|---|---|
GIF |
- |
Global interrupt flag |
Установлен один из нижеперечисленных флагов |
TCF |
DMA_CHn_CIE |
Transfer complete flag |
Пересылка данных завершена |
THF |
DMA_CHn_HIE |
Transfer half flag |
Передана половина данных |
ERRF |
DMA_CHn_EIE |
Transfer error flag |
Ошибка пересылки данных |
Для включения генерации прерывания необходимо:
Выбрать событие (или события) в байте 2 регистра
DMA_CHnA
(n
— номер канала).Разрешить прерывание самого модуля установкой бита
DMA_INT.DMA_IEA
.Разрешить прерывание IRQ#8 от модуля DMA в контроллере прерываний NVIC в регистре
CPU_ISER
.
Взаимодействие модулей DMA и SPI0
При использовании DMA совместно с модулем SPI0 необходимо:
Выполнить настройку модуля SPI0.
Выполнить настройку заданного канала DMA.
Если требуется пересылка из модуля SPI0, установить бит
SPI0_CR0.SPI0_DMA_RXEN
непосредственно перед стартом DMA.Если требуется пересылка в модуль SPI0, установить бит
SPI0_CR0.SPI0_DMA_TXEN
непосредственно перед стартом DMA.Ожидать завершение пересылки (срабатывания флага TCF) на выбранном канале.
После запуска операции пересылки по каналу DMA флаги событий модуля SPI TCF, RXF и TXF маскируются, так что применение блокирующих функций с опросом этих флагов (например, из библиотеки enc28j60.c
) становится невозможным до завершения операции DMA.
Экспериментально установлено, что после завершения операции DMA по отправке данных в SPI работа флагов может не восстановиться. Проблему помогает решить сброс всего модуля SPI0 (настройки модуля при этом сохраняются):
RB(SPI0_CR0_b0) &= 0xFE; // reset SPI0
RB(SPI0_CR0_b0) |= 1;
Контроллер внешних прерываний EXIC
Для своевременного реагирования на события внешних устройств вместе с линиями интерфейса SPI, как правило, требуется отдельная линия для сигнала внешнего прерывания от устройства. Рассмотрим схему формирования сигнала внешнего прерывания модуля EXIC (общая структура контроллера EXIC рассмотрена в первой статье):
Каждый вывод с номером n
каждого порта x
подключен к триггерной схеме управления флагом прерывания, функция которой определяется в поле EXIC_Pxn_TRGS
регистра EXIC_Px_TRGS
:
0 — вывод не приводит к генерации прерывания,
1 — срабатывание по уровню,
2 — срабатывание по фронту,
3 — срабатывание по обоим фронтам.
При выборе функции 1 если бит инверсии Px_CRn.Px_INVn
в настройке вывода сброшен, активный уровень сигнала будет низкий, если бит инверсии установлен — высокий. При выборе функции 2 если инверсия выключена, активным будет спадающий фронт, если инверсия включена — нарастающий.
Все выходы триггерных схем данного порта могут объединяться по схеме «И» (AND) или по схеме «ИЛИ» (OR) для формирования итогового сигнала прерывания INT_Px порта. Подключение выходов к данным схемам определяется масками в регистре EXIC_Px_MSK
.
Модуль EXIC включает следующие общие регистры:
Регистр |
Назначение |
---|---|
|
Флаги прерываний каждого порта Px отдельно для схемы «И» |
|
Биты разрешения прерывания |
|
Дополнительные поля настройки, включая поля управления NMI и разрешения инверсии сигналов прерывания |
Кроме того, имеются регистры EXIC_SRC0
-EXIC_SRC7
, позволяющие определить, какое из внешних прерываний IRQ сработало. Каждый порт включает следующие три регистра (x
— имя порта A-E, n
— номер вывода 0-15):
Регистр |
Назначение |
---|---|
|
Флаги прерывания |
|
Настройки триггера |
|
Маски включения каждого вывода порта Px в схему «И» |
Для настройки внешнего прерывания от вывода n порта Px необходимо:
Настроить вывод порта через регистр
Px_CRn
на нужный режим работы.Настроить режим работы триггера в регистре
EXIC_Px_TRGS
.Включить вывод в схему «И» или схему «ИЛИ» в регистре
EXIC_Px_MSK
.Разрешить внешнее прерывание от порта в регистре
EXIC_INT
.В зависимости от выбранного порта разрешить прерывание IRQ в контроллере прерываний NVIC в регистре
CPU_ISER
согласно таблице:
Порт |
Обозначение прерывания |
Номер прерывания IRQ |
---|---|---|
PA |
EXINT0 |
3 |
PB |
EXINT1 |
4 |
PC |
EXINT2 |
5 |
PD, PE |
EXINT3/EXINT4 |
6 |
Аппаратное вычисление CRC32 на основе модуля GPL
Все МК серии MG32F02 имеют модуль GPL (General Purpose Logic), с помощью которого можно вычислять контрольные суммы CRC8, CRC16 и CRC32 с наиболее распространенными полиномами. В предлагаемом тестировании модуль используется для вычисления CRC32 кадров Ethernet с целью контроля приема данных по шине SPI. Модуль GPL включает 5 регистров, связанных с функцией вычисления CRC, приведенных в таблице (BE - Big Endian).
Регистр |
Назначение |
Описание полей (биты) |
---|---|---|
|
Настройка параметров |
|
|
Включение и настройка параметров |
|
|
Входные данные |
1, 2 или 4 байта исходных данных (в зависимости от поля |
|
Результат вычисления |
Контрольная сумма, полученная в результате накопления данных |
|
Начальное значение |
Начальное значение суммы (Initial value) |
Модуль вычисляет следующие типы контрольных сумм (определяется значением поля GPL_CRC_MDS
):
0 — CCITT16 (полином 0x1021),
1 — CRC8 (полином 0x07),
2 — CRC16 (полином 0x8005),
3 — CRC32 (полином 0x04C11DB7).
Модуль также содержит регистр статуса GPL_STA
, включающий биты результата проверки на четность/нечетность. В МК MG32F02x064 и MG32F02x128 имеются дополнительные регистры для выполнения аппаратного деления. Данные регистры в вычислении CRC не участвуют.
В стандарте IEEE 802.3 Ethernet каждый кадр снабжается 32-битным полем контрольной суммы CRC32, вычисляемой на основе полинома 0xEDB88320. Примеров реализации алгоритма в сети довольно много. Например, можно встретить такой код:
unsigned int crc32(const char* data, size_t length){
const unsigned int POLY = 0xEDB88320; // standard polynomial in CRC32
unsigned int reminder = 0xFFFFFFFF; // standard initial value in CRC32
for(size_t i = 0; i < length; i++){
reminder ^= (unsigned char)data[i]; // must be zero extended
for(size_t bit = 0; bit < 8; bit++)
if(reminder & 0x01)
reminder = (reminder >> 1) ^ POLY;
else
reminder >>= 1;
}
return reminder ^ 0xFFFFFFFF;
}
В документации к МК указан полином 0x04C11DB7, который получается перестановкой всех битов (0-й с 31-м, 1-й с 30-м и т.д.). Соответственно для того, чтобы можно было аппаратно вычислить сумму по нужному алгоритму, необходимо:
переставить биты в исходных данных (8-, 16- или 32-разрядных),
переставить биты в 32-битной сумме,
инвертировать результат.
Для указанной задачи разработана функция crc32_block8()
(файлы ulib.h
и ulib.c
), выполняющая аппаратное вычисление CRC32 кадров Ethernet:
/// Вычисление CRC32 (IEEE 802.3, polynomial 0x04C11DB7, reversed polynom 0xEDB88320)
uint32_t crc32_block8(const void* buf, uint32_t len) {
uint32_t i;
RB(GPL_CR1_b0)=0; // сброс модуля - выполнять обязательно!
RB(GPL_CR0_b0)=GPL_CR0_BREV_MDS_8bit_b0; // перестановка битов в байте
RW(GPL_CRCINIT_w)=0xffffffff; // начальное значение
RB(GPL_CR1_b0)=
GPL_CR1_CRC_BREV_32bit_b0 | // перестановка битов в 32-битной сумме
GPL_CR1_CRC_DSIZE_8bit_b0 | // размер входных данных - 1 байт
GPL_CR1_CRC_MDS_crc32_b0 | // режим CRC32
GPL_CR1_CRC_EN_enable_b0; // включаем модуль
for (i=0; i<len; i++) {
RB(GPL_DIN_b0) = ((uint8_t*)buf)[i];
}
return RW(GPL_DOUT_w) ^ 0xffffffff; // по стандарту Ethernet нужно выполнить инверсию
}
Перед каждым последующим вычислением CRC32 на основе массива данных модуль необходимо сбросить записью 0 в разряд GPL_CR1.GPL_CRC_EN
, после чего записать в этот разряд 1. Если этого не сделать, аккумулирование суммы будет продолжаться с текущего значения. Запись в регистр GPL_CRCINIT
начального значения не приводит к инициализации алгоритма накопления.
Перед использованием модуля GPL необходимо включить его тактирование (функция gpl_init()
, файлы init.h
init.c
):
void gpl_init() {
RH(CSC_KEY_h0) = 0xA217; // unlock access to CSC regs
RH(CSC_AHB_h0) |= CSC_AHB_GPL_EN_enable_h0;
RH(CSC_KEY_h0) = 0; // lock access to CSC regs
}
Ethernet-контроллер ENC28J60
В качестве внешнего SPI устройства в тестировании будем использовать популярный Ethernet-контроллер ENC28J60 компании Microchip, работающий в стандарте 10Base-T. Основными достоинствами микросхемы являются: хорошая функциональность, сравнительно большой встроенный буфер (8 кбайт), простота применения, а также доступность. Из недостатков стоит отметить большое число ошибок, неисправленных вплоть до последних ревизий кристалла, а также значительный потребляемый ток (до 150 мА от источника 3.3 В). Контроллер очень подробно описан в первом приведенном источнике. Рассмотренная в нем библиотека с небольшими доработками под МК серии MG32F02 используется и в наших тестах.
Обмен данными по шине SPI ведётся в режиме 0 (CPOL=0, CPHA=0), а также применяется сигнал внешнего прерывания. Микросхема ENC28J60 поддерживает скорость передачи данных по SPI до 10 Мбит/с. В наших тестах мы ограничимся скоростью 2 Мбит/с.
Тестирование
Аппаратная часть
Целью тестирования является проверка работы МК в роли ведущего и ведомого устройства SPI. В качестве ведущего будет применяться МК MG32F02A064AD48, в качестве ведомого — Ethernet-контроллер ENC28J60 и МК MG32F02A032AT20. Схема подключения микроконтроллеров показана на следующем рисунке.
Для питания схемы в связи с большим током потребления м/с ENC28J60 (до 150 мА) необходимо использовать отдельный источник питания напряжением 3.3 В. К интерфейсу SPI (J4) ведущего МК U1 подключаются либо сетевой модуль на ENC28J60, либо МК U2 (условно показан как разъем J5). Каждый МК, как и в предыдущих тестах, подключается также к отладочному ПК через интерфейс UART (в обоих случаях используется модуль МК URT0).
Проектные файлы
В сегодняшних тестах будет задействован контроллер DMA. Эксперимент показал, что контроллер DMA не может работать адекватно, если программа выполняется в ОЗУ. Видимо это связано с тем, что для DMA просто не остается свободных временных интервалов, когда можно обращаться в память. Поэтому, оставляя метод разделения "прошивки" на базовую и прикладную части, последнюю мы вынуждены перенести в область flash-памяти. В связи с этим в файле premake5.lua
появились следующие цели сборки:
svr64_ram
— базовая часть для МК MG32F02A064, выполняющая переход в прикладную часть в ОЗУ;svr64_rom
— базовая часть для МК MG32F02A064, выполняющая переход в прикладную часть в области flash-памяти;spim
— прикладная часть для ведущего устройства SPI (MG32F02A064),spis
— прикладная часть для ведомого устройства SPI (MG32F02A032).
Соответственно добавлены ld-скрипты mg32f02a064_app_ram.ld
и mg32f02a064_app_rom.ld
для сборки прикладных частей под ОЗУ и под flash-память. Также добавлены скрипты shell для сборки и запуска (загрузки в ОЗУ или "прошивки" в flash): build_svr64_ram
, build_svr64_rom
, build_spim
, build_spis
и run_svr64_ram
, run_svr64_rom
, run_spim
, run_spis
.
Для операций DMA согласно рекомендациям User Guide выделены последние 2 килобайта области ОЗУ. Для прикладной части во flash устанавливается стартовый адрес 0x00001000. В файле premake5.lua
для каждой цели сборки необходимо указать начало области прикладной части в переменной APP_ORIGIN
, например:
defines {"APP_ORIGIN=0x20000000"}
Для взаимодействия с Ethernet-контроллером ENC28J60 в проект добавлена библиотека нижнего уровня ic/enc28j60.c
. Высокоуровневые операции вынесены в библиотеку src/ethernet.c
. Некоторые действия с модулем SPI0 оформлены в виде функций в файлах src/spi.h
и src/spi.c
, а с модулем DMA — в файле src/dma.c
.
Весь исходный код и сопутствующие файлы доступны в репозитории GitHub.
Библиотека для работы с модулем SPI
Рассмотрим функции из файлов src/spi.h
и src/spi.c
.
Функция spi_init()
выполняет инициализацию модуля и настройку тактирования. Выбирается тактовая частота сигнала SCK 2 МГц.
Функция spi_setup_mode()
устанавливает режим работы модуля согласно формату регистра SPI0_CR0
. Для удобства применения функции создано перечисление SPI_Config
, включающее некоторые возможные опции (битовые маски).
Функция spi_setup_int()
разрешает прерывание модуля INT_SPI0 по флагам, указанным в аргументе согласно формату регистра SPI0_INT
. Также разрешается прерывание в модуле NVIC.
Функция spi_nss()
применяется для управления сигналом выбора кристалла NSSO: при значении аргумента "1" сигнал активируется (низкий уровень), при значении "0" — деактивируется (высокий уровень). Функция применяется в ведущем устройстве.
Функции spi_flush_rx()
и spi_flush_tx()
используются для сброса буфера приема и передачи соответственно.
Функции spi_tx()
и spi_rx()
используются для передачи и приема данных по одному байту в ведущем устройстве.
Функции spi_slave_tx()
и spi_slave_rx()
используются для передачи и приема данных по одному байту в ведомом устройстве.
Прием кадров без DMA
Начнем с простого примера приема кадров Ethernet на основе обработчика внешнего прерывания от сетевого модуля ENC28J60 по приему кадра. Составим основную функцию теста spi_test_master()
в файле test/spi_test.c
:
void spi_test_master() {
char s[8];
// Настройка выводов:
HW_SPI0_SETMISO; HW_SPI0_SETMOSI; HW_SPI0_SETSCK; HW_SPI0_SETNSS;
// Инициализация, настройка тактирования:
spi_init();
// Настройка режима работы
spi_setup_mode(
SPI_NSSO_INV | SPI_NSS_SWEN | // software NSS control
SPI_NSSO_EN | SPI_MASTER | SPI_MSB | SPI_CPHA_LE | SPI_CPOL_LOW
| SPI_CR0_DOUT_MDS_enable_w // надо включить, если нет резистора подтяжки
);
RB(SPI0_CR2_b1) = 1; // SPI0_RX_TH = 1
RW(SPI0_CR2_w) |= (8 << SPI_CR2_DSIZE_shift_w); // Размер кадра в битах
spi_flush_rx();
eth_init(addr);
uart_puts(PORT,"EREVID: ",UART_NEWLINE_NONE); strUint16hex(s,enc28j60_rcr(EREVID)); uart_puts(PORT,s,UART_NEWLINE_CRLF);
uart_puts(PORT,"ERXFCON:",UART_NEWLINE_NONE); strUint16hex(s,enc28j60_rcr(ERXFCON)); uart_puts(PORT,s,UART_NEWLINE_CRLF);
uart_puts(PORT,"MAC: ",UART_NEWLINE_NONE); eth_get_addr(s); debugbuf(s,6);
enc28j60_release();
exint_setup();
eth_setup_int();
}
После настройки выводов интерфейса согласно информации из файла hwcf.h
инициализируется модуль SPI0. Далее выполняется настройка режима работы через функцию spi_setup_mode()
. Для генерации сигнала выбора кристалла (Chip Select) будем использовать "штатный" сигнал NSSO с низким активным уровнем (поэтому включаем инверсию). Управлять сигналом NSSO будем программно (SPI_NSS_SWEN
). Модуль SPI0 включаем в режиме ведущего с указанием нужно режима CPHA и CPOL. В завершении настройки устанавливаем однобайтовый режим приема (SPI0_RX_TH
=1 и SPI_CR2_DSIZE
=8) и сбрасываем буфер приема на всякий случай (spi_flush_rx()
).
Далее переходим к инициализации сетевого модуля с указанием локального MAC-адреса, заданного чуть выше в этом же файле:
/// Локальный MAC адрес (сетевой порядок байт)
const uint8_t addr[6]={0x02,0xEE,0x10,0x00,0x00,0x01};
Запрашиваем значения регистров EREVID
(номер ревизии м/с) и ERXFCON
(правила фильтрации кадров), выводим их значения вместе с MAC-адресов в терминал. Далее выполняем настройку внешнего прерывания МК в функции exint_setup()
и разрешаем генерацию прерывания в ENC28J60 (eth_setup_int()
). Рассмотрим функцию exint_setup()
:
/// Настройка внешнего прерывания EXIC
void exint_setup() {
// Настройка вывода и контроллера EXIC
RH(HW_EXINT0_CRH0) =
PX_CR_PU_enable_h0 | // подтяжка
PX_CR_IOM_din_h0; // цифровой вход
RW(EXIC_PA_TRGS_w) |= (2 << (HW_EXINT0_BIT*2)); // Edge
RW(EXIC_PA_MSK_w) |= (1 << HW_EXINT0_BIT); // OR (AOM)
RB(EXIC_INT_b0) |= EXIC_INT_PA_IEA_enable_b0;
// Установка обработчика прерывания:
SVC2(SVC_HANDLER_SET,3,exint0_hdl_nodma);
RW(CPU_ISER_w) = (1 << 3); // SETENA 3
}
Для сигнала прерывания используем вывод PA12 в режиме цифрового входа с подтяжкой. В контроллере EXIC для данного вывода устанавливаем срабатывание по фронту и включаем вывод в схему формирования прерывания "ИЛИ". Далее включаем прерывание EXINT0, устанавливаем обработчик и разрешаем прерывание IRQ#3 в модуле NVIC. Рассмотрим обработчик прерывания exint0_hdl_nodma()
:
/// Обработчик внешнего прерывания без DMA.
void exint0_hdl_nodma() {
led1_on();
enc28j60_bfc(EIE,EIE_INTIE); // сбрасываем INTIE бит согласно даташиту
eth_frame_len=eth_recvpkt(eth_frame,ETH_FRAME_MAXSIZE);
if (eth_frame_len) {
debug('L',eth_frame_len);
debugbuf(eth_frame,14);
}
enc28j60_bfs(EIE,EIE_INTIE); // устанавливаем INTIE бит согласно даташиту
RH(EXIC_PA_PF_h0) = HW_EXINT0_MASK; // сбрасываем флаг (EXIC_PA_PF_PA12_PF_mask_h0)
led1_off();
}
В начале функции в состояние лог. "1" переходит сигнал светодиода D1, который будет использоваться в цифровом анализаторе. Далее запрещается генерация внешнего прерывания от сетевого модуля и вызывается функция приема кадра eth_recvpkt()
в глобальный буфер eth_frame
размером ETH_FRAME_MAXSIZE
байт. В случае успешного получения кадра в терминал с меткой L
выводится его длина, а также hex-дамп первых 14 байт. После этого прерывания сетевого модуля разрешаются, сбрасывается флаг прерывания EXINT0 и выключается светодиод.
Результат работы наблюдаем в терминале:
Hello
EREVID: 0006
ERXFCON:00A1
MAC: 02 EE 10 00 00 01
L 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 9D 08 06
L 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 DC 08 06
L 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 9D 08 06
L 013B 00315
FF FF FF FF FF FF 08 62 66 30 B3 DE 08 00
L 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 DC 08 06
L 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 9D 08 06
Принимаются только широковещательные кадры, поскольку адрес сетевого модуля пока нигде неизвестен. С помощью цифрового анализатора Saleae Logic посмотрим на временные диаграммы приема кадра. На следующем рисунке процесс показан крупным планом:
В момент времени A1 (спадающий фронт сигнала ETH_INT) срабатывает прерывание EXINT0 по приему кадра от ENC28J60. Далее в обработчике прерывания exint0_hdl_nodma()
начинают выполняться команды по запросу сетевого модуля. С момента B1 до B2 (422 мкс) происходит считывание буфера, содержащего данные принятого кадра Ethernet. В течение этого времени сигнал NSS (SPI-ENABLE) остается в активном состоянии согласно работе программы. После этого выполняются завершающие действия функции eth_recvpkt()
до момента A2. Интервал A1-A2 составил 668 мкс. Далее выполняется сравнительно медленный вывод информации в UART, после чего разрешается прерывание в сетевом модуле по команде установки бита EIE_INTIE
(0x9B, 0x80), выключается светодиод и обработчик прерывания завершает работу (на данной диаграмме не показано, см. ниже). Общее время приема кадра длиной 60 байт в данном примере составило 5729 мкс. На следующем рисунке начало взаимодействия МК и сетевого модуля показано более подробно.
Поскольку прием данных выполняется программой по одному байту, между байтами имеется существенный интервал около 2.5 мкс, что показано на следующих диаграммах:
Завершающая стадия показана на следующем рисунке:
Прием кадров в режиме DMA
Создадим функцию для настройки режима работы каналов DMA, которая будет вызываться из spi_test_master()
:
/// Настройки DMA
void spi_dma_setup() {
dma_init();
// Настройка канала 0 для приема
dma_setup(0,0);
dma_setup_sd(0,
DMA_SPI0_RX | // источник
(DMA_MEM_Write << 8) // приемник
);
dma_setup_memdst(0,eth_frame);
// Настройка канала 1 для отправки
dma_setup(1,0);
dma_setup_sd(1,
DMA_MEM_Read | // источник
(DMA_SPI0_TX << 8) // приемник
);
dma_setup_memsrc(1,eth_frame);
// Включаем прерывание по завершению передачи для каналов 0 и 1
RB(DMA_CH0A_b2) = DMA_CH0A_CH0_CIE_enable_b2;
RB(DMA_CH1A_b2) = DMA_CH1A_CH1_CIE_enable_b2;
RB(DMA_INT_b0) = DMA_INT_IEA_enable_b0;
SVC2(SVC_HANDLER_SET,8,dma_hdl);
RW(CPU_ISER_w) = (1 << 8); // SETENA 8
}
Вначале происходит инициализация модуля DMA (включение тактирования и самого модуля). Далее канал 0 DMA настраивается для приема данных из модуля SPI0 и их отправки в область ОЗУ по адресу eth_frame
. Канал 1 настраивается для чтения данных из ОЗУ с этого же адреса и их отправки в модуль SPI0 для последующей передачи. Затем разрешаются прерывания по завершению передачи для обоих каналов и прерывание модуля DMA. Далее устанавливается обработчик прерывания и IRQ#8 разрешается в модуле NVIC.
Процесс приема кадра Ethernet можно разделить на 3 этапа:
выполнение обработчика внешнего прерывания
exint0_hdl_dma()
после получения нового кадра модулем ENC28J60, в котором выполняются подготовительные действия для получения данных самого кадра по каналу DMA;прием данных кадра Ethernet из модуля SPI0 в ОЗУ по каналу DMA;
выполнение обработчика прерывания DMA
dma_hdl()
по завершению приема данных из модуля SPI0.
Рассмотрим обработчик exint0_hdl_dma()
:
/// Обработчик внешнего прерывания с DMA.
/// Инициализация процедуры считывания принятого пакета.
void exint0_hdl_dma() {
uint16_t rxlen, status;
led1_on();
enc28j60_bfc(EIE,EIE_INTIE); // сбрасываем INTIE бит согласно даташиту
// Считываем заголовок:
enc28j60_wcr16(ERDPT, enc28j60_rxrdpt); // Устанавливаем начальный адрес в буфере для считывания
enc28j60_read_buffer((void*)&enc28j60_rxrdpt, sizeof(enc28j60_rxrdpt));
enc28j60_read_buffer((void*)&rxlen, sizeof(rxlen));
enc28j60_read_buffer((void*)&status, sizeof(status));
// Кадр принят успешно?
if (status & 0x0080) { //success
// Читаем пакет в буфер (если буфера не хватает, пакет обрезается)
eth_frame_len = (rxlen > ETH_FRAME_MAXSIZE) ? ETH_FRAME_MAXSIZE : rxlen;
enc28j60_select();
enc28j60_tx(ENC28J60_SPI_RBM);
// Далее необходимо принять из SPI eth_frame_len байт
spi_dma_start_rx(eth_frame_len); // запускаем прием данных кадра через DMA
}
// Функции enc28j60_* далее применять нельзя до завершения операции DMA !!!
RH(EXIC_PA_PF_h0) = HW_EXINT0_MASK; // сбрасываем флаг (EXIC_PA_PF_PA12_PF_mask_h0)
}
В отличие от предыдущего варианта обработчика без DMA, в данной функции выполняется часть кода из функции eth_recvpkt()
. Вначале считывается заголовок кадра Ethernet и статус операции. Если операция получения кадра завершена успешно, в глобальной переменной eth_frame_len
устанавливается общая длина массива данных, которые нужно принять, включая данные кадра и 4 байта поля CRC32. После этого активируется сигнал NSSO (выбор кристалла) и запускается процедура приема данных по каналу DMA через функцию spi_dma_start_rx()
:
/// Запуск процедуры приема данных на канале 0, n - число байт
void spi_dma_start_rx(uint32_t n) {
RB(SPI0_CR0_b3) |= SPI_CR0_DMA_RXEN_enable_b3;
dma_setup_amount(0,n);
dma_start(0,DMA_CH0A_CH0_BSIZE_one_b1);
}
В функции выполняются следующие действия:
в модуле SPI разрешается управление приемом данных со стороны контроллера DMA,
устанавливается объем пересылаемых данных (длина кадра Ethernet),
запускается пересылка по каналу 0 DMA.
С этого момента до завершения операции пересылки данных по каналу DMA отключается работа флагов модуля SPI, поэтому вызов каких-либо функций из библиотеки enc28j60.c
недопустим.
В функции exint_setup()
заменим обработчик прерывания на вариант с DMA:
// Установка обработчика прерывания:
SVC2(SVC_HANDLER_SET,3,exint0_hdl_dma);
Теперь рассмотрим обработчик dma_hdl()
:
/// Обработчик прерывания DMA.
void dma_hdl() {
uint32_t f;
f = RW(DMA_STA_w);
if (f & DMA_STA_CH0_TCF_happened_w) {
// Завершен прием данных по SPI
enc28j60_release();
// Устанавливаем ERXRDPT на адрес следующего пакета - 1 (минус 1 - из-за бага)
enc28j60_wcr16(ERXRDPT, (enc28j60_rxrdpt - 1) & ENC28J60_BUFEND);
enc28j60_bfs(ECON2, ECON2_PKTDEC); // Сбрасывает флаг PKTIF, если счетчик ==0
RB(DMA_CH0A_b3) |= DMA_CH0A_CH0_TC2F_mask_b3; // clear flag
// Разрешаем следующее прерывание ENC28J60:
enc28j60_bfs(EIE,EIE_INTIE); // устанавливаем INTIE бит согласно даташиту
__disable_irq(); // опционально: запрещаем все прерывания МК до прочтения флагов state
state |= ST_PKTRECV;
}
if (f & DMA_STA_CH1_TCF_happened_w) {
// Завершена отправка данных по SPI
while (!(RB(SPI0_STA_b0) & SPI_STA_TCF_happened_b0)); // дожидаемся фактического завершения передачи
enc28j60_release();
RB(DMA_CH1A_b3) |= DMA_CH0A_CH0_TC2F_mask_b3; // clear flag
__disable_irq(); // опционально: запрещаем все прерывания МК до прочтения флагов state
state |= ST_PKTSENT;
}
}
Вначале по флагам определяется, по какому каналу DMA завершилась операция пересылки данных. В данном тесте используется канал 0, обработка события на канале 1 будет применяться для следующего теста при отправке кадров. По завершению приема данных по SPI деактивируется сигнал NSSO, после чего выполняются завершающие операции в ENC28J60 и сбрасывается флаг прерывания DMA. Далее разрешается генерация сигнала прерывания в ENC28J60 и устанавливается прикладной флаг ST_PKTRECV
для главного цикла программы. Опционально запрещаются прерывания МК до завершения обработки события в главном цикле.
После настройки DMA в функции spi_test_master()
начинается основной цикл:
spi_dma_setup();
while (1) {
if (state & ST_PKTRECV) {
// Принят новый кадр
eth_frame_len-=4;
debug('R',eth_frame_len);
debugbuf(eth_frame,14);
debug32hex('C',*(uint32_t*)(eth_frame+eth_frame_len)); // CRC32 - little endian!
debug32hex('S',crc32_block8(eth_frame,eth_frame_len));
state &= ~ST_PKTRECV;
__enable_irq();
}
}
При активации флага ST_PKTRECV
в терминал выводятся:
сообщение с меткой
R
о приеме кадра с указанием его длины (без учета поля с CRC32);дамп первых 14 байт кадра, включая адреса отправителя и получателя и тип кадра (или длину);
сообщение с меткой
C
с полученной из кадра контрольной суммой;сообщение с меткой
S
с контрольной суммой, вычисленной с помощью ранее рассмотренной функцииcrc32_block8()
.
В терминале получаем результат работы (в течение нескольких секунд):
R 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 DC 08 06
C 2AF92CBF
S 2AF92CBF
R 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 9D 08 06
C 9A136A7A
S 9A136A7A
R 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 DC 08 06
C 2AF92CBF
S 2AF92CBF
R 003C 00060
FF FF FF FF FF FF 00 C0 EE 2F A0 18 81 37
C A9D81A55
S A9D81A55
R 003C 00060
FF FF FF FF FF FF 00 C0 EE 2F A0 18 81 37
C 015EABC4
S 015EABC4
Видно, что контрольные суммы совпадают, а значит кадры принимаются правильно и сумма CRC32 вычисляется корректно.
Наш сетевой адаптер настроен на прием "своих" и широковещательных кадров. Последние чаще всего представляют собой ARP-запросы, которые, к тому же легко генерировать на ПК через команду ping
на несуществующий или уже удаленный из ARP-таблицы адрес. Для разбора ARP-запросов создадим простейшую функцию arp_get_ipaddr()
, которая будет возвращать IP адрес из принятого кадра:
/// Возвращает IPv4 адрес из входящего кадра ARP запроса.
/// Если кадр не содержит ARP-запрос, возвращает 0.
uint32_t arp_get_ipaddr(const uint8_t* pkt) {
if (
(*(uint16_t*) (pkt+12) == 0x0608) // проверяем код протокола 0x0806 (big endian)
&& (*(uint16_t*) (pkt+20) == 0x0100)) // проверяем код операции (1-запрос, 2-ответ) (big endian)
{
return __REV(*(uint32_t*)(pkt+38)); // возвращаем адрес в формате little endian
}
return 0;
}
В основном цикле программы будем выводить в терминал: 1) сообщение с меткой R
о приеме кадра с указанием его длины без поля с CRC32, 2) дамп всего кадра, 3) IP адрес с меткой A
, если кадр является ARP-запросом, а вывод контрольной суммы уберем:
while (1) {
if (state & ST_PKTRECV) {
// Принят новый кадр
eth_frame_len-=4;
debug('R',eth_frame_len);
debugbuf(eth_frame,eth_frame_len);
if (a=arp_get_ipaddr(eth_frame)) {
debug32hex('A',a);
}
state &= ~ST_PKTRECV;
__enable_irq();
}
На ПК в программе Wireshark установим фильтр eth.type==ARP
и будем наблюдать активность в сети, в которую также подключен модуль на ENC28J60:
В терминале получаем такой результат (дамп кадров перенесен на вторую строку):
R 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 9D 08 06 00 01 08 00 06 04 00 01 00 00 1B 0D F4 9D 00 00 00 00 00 00 00 00 00 00
C0 A8 00 A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
A C0A800A0
R 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 DC 08 06 00 01 08 00 06 04 00 01 00 00 1B 0D F4 DC 00 00 00 00 00 00 00 00 00 00
C0 A8 00 A1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
A C0A800A1
R 003C 00060
FF FF FF FF FF FF 00 00 1B 0D F4 9D 08 06 00 01 08 00 06 04 00 01 00 00 1B 0D F4 9D 00 00 00 00 00 00 00 00 00 00
C0 A8 00 A0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
A C0A800A0
Видно, что в сети проходили ARP-запросы на адреса 192.168.0.160 и 192.168.0.161. Дамп соответствующих кадров в терминале и в Wireshark совпадают, функция приема данных на основе DMA работает корректно.
Отправка кадров Ethernet
Теперь перейдем к отправке кадров Ethernet. Реализуем на МК имитацию узла сети IPv4 и простейшую функцию arp_gen_reply()
генерации пакетов с ARP-ответом:
/// Формирует кадр с ARP-ответом.
void arp_gen_reply(uint8_t* pkt) {
memcpy(eth_frame+32,eth_frame+6,6); // target MAC
memcpy(eth_frame+38,eth_frame+28,4); // target IP
memcpy(eth_frame,eth_frame+6,6); // Dest ADDR
eth_setframe_addr(addr); // Src ADDR
eth_frame[21]=2; // reply opcode
memcpy(eth_frame+22,addr,6); // < sender MAC
*(uint32_t*)(eth_frame+28)=__REV(ipaddr); // < sender IP
}
Локальный адрес узла на МК зададим в глобальной переменной:
/// Локальный IPv4 адрес 192.168.0.177
const uint32_t ipaddr=0xC0A800B1;
В основном цикле программы при активации флага ST_PKTRECV
добавим проверку адреса в ARP-запросе и в случае его равенства установленному в переменной ipaddr
:
вывод сообщения
"> ARP_REPLY"
,генерацию кадра с ARP-ответом в массиве
eth_frame
(длина кадра 42 байта),запуск процедуры отправки кадра на ENC28J60 через функцию
spi_dma_start_tx()
.
В этом же цикле при активации флага ST_PKTSENT
при завершении передачи данных в буфер ENC28J60 по шине SPI добавим:
вывод сообщения
"T PKTSENT"
,сброс модуля SPI0 для восстановления работы флагов,
установку границ кадра на отправку в буфере ENC28J60 и отправку команды на передачу кадра в сеть,
выключение светодиода для наблюдения завершения процедуры отправки в цифровом анализаторе.
Код основного цикла получается следующий:
while (1) {
if (state & ST_PKTRECV) {
// Принят новый кадр
eth_frame_len-=4;
debug('R',eth_frame_len);
debugbuf(eth_frame,eth_frame_len);
if (a=arp_get_ipaddr(eth_frame)) {
debug32hex('A',a);
if (a==ipaddr) {
uart_puts(PORT,"> ARP_REPLY",UART_NEWLINE_CRLF);
arp_gen_reply(eth_frame);
spi_dma_start_tx(42);
}
}
state &= ~ST_PKTRECV;
__enable_irq();
}
if (state & ST_PKTSENT) {
uart_puts(PORT,"T PKTSENT",UART_NEWLINE_CRLF);
// Возвращаем работу флагов SPI:
RB(SPI0_CR0_b0) &= 0xFE; // reset SPI0
RB(SPI0_CR0_b0) |= 1; // reset SPI0
// Буфер в ENC28J60 записан, теперь даем команду на отправку кадра в сеть
// Устанавливаем границы кадра на отправку в буфере ENC28J60:
enc28j60_wcr16(ETXST, ENC28J60_TXSTART);
enc28j60_wcr16(ETXND, ENC28J60_TXSTART + eth_frame_len);
enc28j60_bfs(ECON1, ECON1_TXRTS); // Даем саму команду
led1_off(); // сигнал для цифрового анализатора о завершении процедуры отправки кадра
state &= ~ST_PKTSENT;
__enable_irq();
}
}
Рассмотрим функцию запуска процедуры отправки данных spi_dma_start_tx()
:
/// Запуск процедуры отправки данных на канале 1, n - число байт
void spi_dma_start_tx(uint32_t n) {
uint8_t cbyte=0; // управляющий байт
led1_on(); // сигнал для цифрового анализатора о начале процедуры отправки кадра
while(enc28j60_rcr(ECON1) & ECON1_TXRTS) {
if(enc28j60_rcr(EIR) & EIR_TXERIF) {
enc28j60_bfs(ECON1, ECON1_TXRST);
enc28j60_bfc(ECON1, ECON1_TXRST);
}
}
enc28j60_wcr16(EWRPT, ENC28J60_TXSTART);
enc28j60_write_buffer(&cbyte, 1);
enc28j60_select();
enc28j60_tx(ENC28J60_SPI_WBM);
eth_frame_len=n;
RB(SPI0_CR0_b3) |= SPI_CR0_DMA_TXEN_enable_b3;
dma_setup_amount(1,n);
dma_start(1,DMA_CH0A_CH0_BSIZE_one_b1);
}
В начале функции включается светодиод для отметки момента времени в цифровом анализаторе. Далее в цикле while
происходит обработка бага ENC28J60 (см. функцию eth_sendpkt()
). После этого выполняются действия:
записывается служебный заголовок в буфер ENC28J60 с нулевым управляющим байтом,
активируется сигнал NSSO,
в модуле SPI разрешается управление передачей данных со стороны контроллера DMA,
устанавливается объем пересылаемых данных
n
,запускается пересылка по каналу 1 DMA.
На ПК инициируем отправку ARP-запроса с помощью команды ping
на узел 192.168.0.177, MAC-адрес которого еще не был известен:
ping 192.168.0.177
В Wireshark наблюдаем ARP-запрос с ПК и ответ с МК:
В терминале получаем такой результат:
R 003C 00060
FF FF FF FF FF FF 08 62 66 30 B3 DE 08 06 00 01 08 00 06 04 00 01 08 62 66 30 B3 DE C0 A8 00 0B 00 00 00 00 00 00
C0 A8 00 B1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
A C0A800B1
> ARP_REPLY
T PKTSENT
После этого на ПК в текущей ARP-таблице наблюдаем новую запись с нашим узлом с корректными значениями IP и MAC адресов:
ip n
192.168.0.177 dev eth0 lladdr 02:ee:10:00:00:01 REACHABLE
Если требуется повторить эксперимент, удалить запись о нашем узле можно с помощью команды:
sudo ip n del 192.168.0.177 dev eth0
Посмотрим на временные диаграммы процесса отправки кадра. На следующем рисунке процесс показан крупным планом:
В момент времени A1 начинается отправка команд на ENC28J60 в функции spi_dma_start_tx()
для подготовки буфера передачи сетевого модуля, после чего запускается процедура отправки данных по каналу DMA. По завершению отправки активируется прикладной флаг ST_PKTSENT
и выводится сообщение в терминал, на что тратится существенное время порядка 880 мкс. После этого в сетевой модуль подаются команды на фактическую отправку кадра в сеть и взаимодействие МК с модулем ENC28J60 завершается в момент A2. На следующем рисунке начало отправки данных показано более подробно.
Начиная примерно с отметки времени +80 мкс запускается процедура передачи данных по каналу DMA после программной активации сигнала NSSO и выдачи команды "Write Buffer Memory" (0x7A):
Передача данных в модуле SPI, принимаемых по каналу DMA, происходит байт за байтом без каких-либо межсимвольных интервалов на заданной тактовой частоте сигнала SCK. По завершению передачи активируется флаг TCF модуля DMA и генерируется прерывание. В обработчике прерывания деактивируется сигнал NSSO.
Далее в сетевой модуль отправляется последовательность команд для установки границ буфера. Завершается процесс отправки кадра передачей команды "Transmit Request to Send" установкой бита ECON1_TXRTS
в модуле ENC28J60 (0x9F, 0x08):
Реализация ведомого устройства
Рассмотрим ведомое SPI-устройства на МК MG32F02A032, реализующее отправку произвольного числа байт из своего буфера. Протокол обмена следующий: ведущий отправляет один байт, в котором указывает число запрашиваемых байт. Со следующего кадра шины ведомый начинает отправку запрошенных данных. Для ведущего создадим функцию spi_test_master2()
:
void spi_test_master2() {
uint16_t i;
uint16_t n=5; // Число запрашиваемых байт
char s[8]; // Буфер для данных
s[n]=0; // конец строки
// Настройка выводов:
HW_SPI0_SETMISO; HW_SPI0_SETMOSI; HW_SPI0_SETSCK; HW_SPI0_SETNSS;
// Инициализация, настройка тактирования:
spi_init();
// Настройка режима работы
spi_setup_mode(
SPI_NSS_PEN |
SPI_NSSO_EN | SPI_MASTER | SPI_MSB | SPI_CPHA_LE | SPI_CPOL_LOW
| SPI_CR0_DOUT_MDS_enable_w // надо включить, если нет резистора подтяжки
);
RB(SPI0_CR2_b2) = 8; // DSIZE, размер кадра в битах
while (1) {
spi_tx(n);
delay(20); // даем время слейву на подготовку данных
for (i=0; i<n; i++) s[i]=spi_rx();
uart_puts(PORT,s,UART_NEWLINE_CRLF);
delay_ms(250);
}
}
В этом тесте какие-либо прерывания или DMA не используются, процесс отправки и получения данных происходит в главном цикле. В отличие от предыдущих примеров в данной реализации "мастера" будем использовать автоматическую активацию сигнала NSSO. Для этого в функции spi_setup_mode()
добавим флаг SPI_NSS_PEN
. В переменной n
указывается число запрашиваемых байт, в массиве s
полученные данные сохраняются, после чего строка выводится в терминал и выдерживается пауза около 250 мс. Перед приемом данных выдерживается пауза (delay(20)
) для того, чтобы ведомый успел поместить запрашиваемые данные в свой буфер. Из "слейва" будем ожидать последовательность ASCII-символов.
Перейдем к реализации ведомого устройства, для чего создадим отдельный файл app_spis.c
, в котором реализуем весь основной код. Прием данных будет обрабатываться в прерывании. Контроллер DMA использоваться не будет, поэтому прикладную часть можно поместить в ОЗУ. Рассмотрим основную функцию spi_test_slave()
:
void spi_test_slave() {
// Настройка выводов:
HW_SPI0_SETMISO; HW_SPI0_SETMOSI; HW_SPI0_SETSCK; HW_SPI0_SETNSS;
// Инициализация, настройка тактирования:
spi_init();
// Настройка режима работы
spi_setup_mode(
SPI_NSSI_EN | SPI_SLAVE | SPI_MSB | SPI_CPHA_LE | SPI_CPOL_LOW
);
RW(SPI0_CR2_w) =
SPI_CR2_TXUPD_EN_enable_w | // включаем прямое обновление буфера TX, чтобы успевать обновлять данные
(8 << SPI_CR2_DSIZE_shift_w) | // размер кадра в битах
SPI_CR2_RX_TH_1_byte_w; // порог приема
// Установка обработчика прерывания:
SVC2(SVC_HANDLER_SET,24,spi_hdl);
// Разрешение прерывания, в том числе, в NVIC:
spi_setup_int(SPI_INT_RX_IE_enable_h0);
while (1) ;
}
Вначале происходит настройка выводов и модуля SPI0 аналогично "мастеру", но в функции spi_setup_mode()
указываются флаг SPI_NSSI_EN
для включения автоматического принятия данных при активации входного сигнала NSSI и флаг SPI_SLAVE
для выбора основного режима работы.
Особенностью реализации ведомого является необходимость быстрой записи данных в буфер SPI0_TDAT
на отправку после получения команды. По-умолчанию, первый байт из буфера SPI0_TDAT
копируется в промежуточный буфер еще в момент приема последнего бита предыдущего кадра, т.е. фактически до активации флага RXF. Чтобы "слейв" мог подготовить данные на отправку после приема командного байта, необходимо включить опцию прямого обновления буфера передачи SPI0_TXUPD_EN
в регистре SPI0_CR2
и передавать данные в буфер SPI0_TDAT
как можно быстрее, чтобы успеть к началу следующего кадра.
Далее устанавливается обработчик прерывания по приему данных spi_hdl()
и разрешается прерывание в модулях SPI0 и NVIC. Рассмотрим обработчик прерывания:
/// Обработчик прерывания INT_SPI0
void spi_hdl() {
uint8_t i;
uint8_t n; // Число запрашиваемых байт
mark_on();
n=RB(SPI0_RDAT_b0);
spi_flush_rx();
if (n<TXBUF_SIZE) {
spi_flush_tx();
for (i=0; i<n; i++) spi_slave_tx(s[i]);
}
mark_off();
if (RW(SPI0_STA_b1) & SPI_STA_ROVRF_mask_b1) {
mark_on(); mark_off();
spi_flush_rx(); // сбрасываем буфер приема
RW(SPI0_STA_w) = SPI_STA_ROVRF_mask_w; // сбрасываем флаг
}
}
В начале считывается один байт в переменную n
, входящий буфер очищается (на всякий случай) и, если n
меньше размера буфера TXBUF_SIZE
, на передачу отправляются n
байт. Фактически, по первому байту, удовлетворяющему этому условию, происходит синхронизация работы ведомого с потоком байт от ведущего. В функции spi_slave_tx()
после передачи в буфер SPI0_TDAT
каждого байта ожидается активация флага TXF. Для отладки в цифровом анализаторе указанные действия отмечаются активацией и деактивацией отладочного сигнала MARK (вывод PB8).
Поскольку при отправке данных одновременно происходит и прием, входной буфер может переполняться, после чего флаг RXF перестает активироваться и перестает срабатывать прерывание. Чтобы это не происходило, в обработчике также проверяется флаг ROVRF и в случае его активности сбрасывается он и сам буфер. Данное событие будет отображаться на временной диаграмме сигнала MARK коротким положительным импульсом.
В обработчике используются функции mark_*()
для управления отладочным сигналом MARK и глобальный буфер данных:
inline
void mark_on() {RH(PB_SC_h0) = (1 << 8);}
inline
void mark_off() {RH(PB_SC_h1) = (1 << 8);}
#define TXBUF_SIZE 16
char s[TXBUF_SIZE]="ABCDEFGHIJKLMNO"; // Буфер для данных
Запускаем программы на обоих МК и смотрим терминал "мастера" при запросе 5 байт:
ABCDE
ABCDE
ABCDE
Периодичность не нарушается, значит программа работает правильно. Посмотрим на временную диаграмму обмена данными:
Видно, что прерывание сработало спустя около 5 мкс после фактического получения первого байта и дополнительная задержка в "мастере" перед приемом данных оказалась кстати. Функция spi_slave_tx()
завершилась после фактической отправки третьего байта. После этого (короткий импульс MARK) было обнаружено переполнения буфера (поскольку установлен порог SPI0_RX_TH
на 1 байт), флаг ROVRF и буфер приема были сброшены. Следующие срабатывания прерывания пришлись на моменты после отправки 4-го и 5-го байтов. В обоих случаях был принят байт 0xFF, который был проигнорирован. Переполнения не происходило.
Если в обработчике прерывания отключить сброс флага ROVRF, ведомый перестанет работать правильно. Посмотрим, что произойдет, если сброс флага оставить, но не сбрасывать буфер приема (spi_flush_rx()
):
Видно, что переполнение было зафиксировано дважды: второй раз при втором срабатывании прерывания. Оба варианта программы "слэйва" работают правильно.
Заключение
Сегодня мы протестировали модуль SPI0 в режиме ведущего и ведомого, применили контроллер DMA для пересылки данных между модулем SPI0 и ОЗУ, использовали внешнее прерывание от Ethernet-контроллера ENC28J60 и имитировали узел локальной сети. За рамками статьи осталось применение модуля SPI0 в режимах Dual, Quad и Octal, а также работа на удвоенной скорости обмена. Все эти вопросы читатель может изучить по User Guide микроконтроллеров серии MG32F02.
lamerok
Жесть, особенно запрет всех прерываний в обработчика прерывания и разрешение обратно где - то в основном потоке. Также цикл for по количеству байт в обработчике прерывания.
Я надеюсь это просто для тестов(но непонятно зачем так делать и так усложнять код) и в продукт такое не идёт.
ИМХО, Такой код сам по себе сложен для понимания. А если надо будетъ что оо поменять или добавить? , я так понимаю придётся всё переписать. Не надо так.
reug Автор
Целью статьи является рассмотрение модулей МК, а не создание какого-либо продукта, и даже не библиотеки. Исходный код приведен исключительно для тестирования функций МК.
Чтение кадра в обработчике прерывания от ENC28J60 с помощью функции eth_recvpkt() показано для примера "без DMA" как раз для того, чтобы сравнить преимущества варианта с DMA.
В обработчике dma_hdl() установка прикладных флагов делается через запрещение прерывания, чтобы обеспечить атомарность их обработки в основной программе, но это совершенно опционально.
lamerok
Ок, но даже пример, должен быть понятным
Зачем в прерывании то?
Тогда и разрешение должно быть сразу после установки,а не в другом потоке, кроме того, у вас что вложенные прерывания есть, где эти флаги считываются? Или вообще в чем смысл? Это же путает основательно, если вы ради примера, то зачем вообще это показывать, просто флаги установили и ок.