nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

Мучаем 5576ХС4Т - часть 'h20 - работа с АЦП ADC124s051

Часть 0 - покупаем, паяем, ставим драйвера и софт
Часть 1 - что это вообще за зверь?
Часть 2 - наша первая схема!
Часть 3 - кнопочки и лампочки
Часть 4 - делитель частоты
Часть 5 - подавление дребезга кнопки
Часть 6 - заканчиваем кнопочки и лампочки
Часть 7 - счетчики и жаба
Часть 8 - передатчик UART
Часть 9 - Hello, wolf!
Часть 'hA - приёмник UART
Часть 'hB - UART и жаба
Часть 'hC - полудуплексный UART.
Часть 'hD - МКО (МКИО, Mil-Std 1553) для бедных, введение.
Часть 'hE - приёмопередатчик МКО "из подручных материалов" (в процессе)
Часть 'hF - модуль передатчика МКО
Часть 'h10 - передатчик сообщений МКО
Часть 'h20 - работа с АЦП ADC124s051
Часть 'h21 - преобразование двоичного кода в двоично-десятичный (BCD)
Часть 'h22 - Bin2Bcd с последовательной выдачей данных
Часть 'h23 - перемножитель беззнаковых чисел с округлением
Часть 'h24 - перемножитель беззнаковых чисел, реализация
Часть 'h25 - передаём показания АЦП на компьютер
Часть 'h26 - работа над ошибками (быстрый UART)
Часть 'h27 - PNG и коды коррекции ошибок CRC32
Часть 'h28 - передатчик изображения PNG
Часть 'h29 - принимаем с ПЛИС изображение PNG
Часть 'h2A - ZLIB и коды коррекции ошибок Adler32
Часть 'h2B - ускоряем Adler32
Часть 'h2C - формирователь потока Zlib
Часть 'h2D - передаём сгенерированное PNG-изображение
Часть 'h2E - делим отрезок на равные части
Часть 'h2F - знаковые умножители, тысячи их!
Часть 'h30 - вычислитель множества Мандельброта
Часть 'h31 - ускоренные сумматоры
Часть 'h32 - ускоренные счётчики (делаем часы)
Часть 'h33 - ускоряем ВСЁ
Часть 'h34 - ускоренные перемножители
Часть 'h35 - умножители совсем просто
Часть 'h36 - уравновешенный четверичный умножитель

Извините, приёмопередатчик МКО пока что "подвис в воздухе", к нему мы непременно вернёмся, возможно даже, в ближайший месяц.

А пока разберёмся, как работать с ADC124s051 - это 12-битная АЦП, позволяющая выдавать до 500 000 выборок в секунду, причём перед каждой выборкой можно задать, какой из 4 входных каналов использовать. Данная АЦП уже входит в состав отладочной платы и подключена к ПЛИС по шине SPI.



Как мы видим из принципиальной схемы, АЦП запитывается от источника образцового напряжения 3 вольта. На её 4 входа можно подавать напряжения от 0 до 3 вольт, это и есть её рабочий диапазон (точнее, 3 вольта ровно должны были бы соответствовать числу 0x1000, тогда как максимально возможное для 12-битного АЦП 0x0FFF соответствует напряжению 2,999267 вольта).

Два из входов доступны только в виде "контрольных точек" на плате, я туда запаял шину 1,8 вольта (ядро ПЛИС) и, через резистор 1,8 кОм - шину 3,3 вольта (периферия ПЛИС), чтобы посмотреть, насколько они стабильны.

К ПЛИС поступает 4 провода: nCS (negative Chip Select), SCLK (Synchronous CLocK), MISO (Master In Slave Out) и MOSI (Master Out Slave In).

Пока мы не используем АЦП, рекомендуется подать на nCS лог. "1" - тогда чип уйдёт в спящий режим, потребляя всего 38 нА (НАНОампер!), и, как обычно, он "освободит" MISO, переведя свой выходной каскад в режим высокого выходного сопротивления (Z).

Когда мы решили воспользоваться АЦП, то первым делом подаём на nCS лог. "0", после чего выжидаем хотя бы 10 нс и можем запустить тактовую частоту на SCLK (при условии, что до этого подавали лог. "0". Можно хотя бы за 10 нс до начала работы подать лог. "1" и начать с отрицательного фронта). АЦП будет работать при тактовой частоте от 50 кГц до 16 МГц, но рекомендуется использовать диапазон от 3,2 МГц до 8 МГц - именно тогда он будет обеспечивать точности, прописанные в документации.



Один цикл измерений состоит из 16 периодов SCLK: по положительному фронту микросхема будет "защёлкивать" биты, которые мы подаём на MOSI, из которых информативных всего лишь 2: ADD0 и ADD1. Они и выражают номер входного канала, который мы хотим оцифровать. ADD2 должен быть нулевым (видимо, "задел" на будущие АЦП, где каналов будет 8), а DONTC означает Don't Care - можно подавать что угодно.

После 4-го периода SCLK, АЦП начнёт передавать оцифрованные биты, от старшего к младшему. Отметим, что будет использован канал, выбранный на предыдущем цикле измерений, а те биты, что мы ещё продолжаем передавать, пока ещё "не вступили в силу"!

По окончании 16 периодов SCLK, мы можем сразу же подать лог. 1 на nCS - к этому моменту мы уже получили все 12 бит, да и микросхемке от нас "ничего не надо".

Мне показалось, что для управления этим АЦП удобнее всего сделать "специализированный" контроллер SPI - с ним тупо удобнее работать, и он будет значительно компактный, чем "байт-ориентированный" стандартный.

Вот заголовок нашего модуля:
module SPI_for_ADC124s051 (input clk,
                           input ce,
                           input [1:0] Ain,
                           input start,
                           inout MISO,
			   output nCS,
                           output SCK,
                           output MOSI,
                           output reg [11:0] Q = 1'b0,
                           output finished,
                           output reg [1:0] Chan = 2'b00);


clk - тактовая частота, как обычно.
ce - единичные импульсы, поступающие с удвоенной частотой SCLK. Например, если мы хотим работать на частоте 8 МГц, то сюда надо подавать импульсы с частотой 16 МГц, В прошлые разы мы ставили делитель частоты непосредственно внутрь модуля приёмопередатчика, а тут решили "вытащить наружу". Мотивация такая - если в UART или в МКО (в качестве оконечного устройства или монитора шины) нам было необходимо "обнулять" делитель частоты по приходу входного сигнала, то здесь мы "хозяева", поэтому можем допустить, что ce "тикает" себе спокойненько, независимо от нас, а когда надо - дождёмся ближайшего импульса ce и начнём работать. Такой подход позволяет немножечко сэкономить на делителях частоты, а в дальнейшем, возможно, и упростить взаимодействие с оперативной памятью. Рано или поздно нам захочется данные с разнообразной "периферии" занести в память, и если эта память "однопортовая" (только один порт на запись), то обращение различных устройств к памяти хочется "разнести".
Ain - номер входного канала, с которого мы хотим получить данные в следующий раз.
start - запускающий импульс. Как всегда, именно в этот момент мы "защёлкнем" значение Ain и начнём работать.
MISO - Master In Slave Out. Заметим, мы наконец-то применили ключевое слово inout - вход/выход. Поскольку микросхема АЦП, когда отключена, позволяет этому проводу "повиснуть в воздухе", то мы захотим "притянуть" его на общий провод, потому что ненавидим висящие в воздухе провода! Пока я работал с другой АЦП (она выдавала 8 бит параллельно), то наблюдал, как при отключенной АЦП светодиоды начинали "тлеть", а потребление всей схемы в этот момент скакнуло на 20..30 мА за счёт сквозных токов!
nCS - negative Chip Select.
SCK - то же самое, что SCLK, иногда так называют, иногда эдак.
MOSI - Master Out Slave In.
Q - здесь появится 12-битное оцифрованное значение, но только на один такт, пока finished = 1.
finished - сигнал окончания работы.
Chan - номер канала, с которого был взят данный сигнал. Очень удобно, когда мы задействуем больше одного канала - мы можем из Chan сформировать адрес в памяти, куда поместить Q, так что значения с разных каналов будут автоматически "раскидываться" по нужным местам.

Для хранения входных и выходных данных нам достаточно одного 12-битного сдвигового регистра! В начале работы в него будут "защёлкнуты" 2 бита для выбора канала, а затем по каждому импульсу SCLK он будет сдвигаться влево, старший бит будет отправляться в MOSI, а бит, принятый с MISO, будет заноситься в младший бит сдвигового регистра. К концу работы в нашем регистре будет содержаться 12 бит результата. Собственно, этот регистр мы и объявили в заголовке - это и есть выход Q.

Впрочем, в действительности работа будет вестись чуть хитрее - производить сдвиг мы хотим по отрицательному фронту SCK, чтобы новый бит появлялся в MOSI "заблаговременно" - аж за полтакта! А вот заносить новое значение MISO в регистр мы хотим по положительному фронту, потому что именно в этот момент там обязаны находиться корректные данные.

Но начать надо, как обычно, с "командоаппарата", который будет отсчитывать наши такты. Нам нужно состояние idle (режим ожидания) и 16 состояний, соответствующих 16 периодам SCK. Ещё одно состояние - пауза, когда мы уже "сбросили в ноль" nCS, но должны выдержать хоть немного времени, прежде чем запустить SCK. В случае данной АЦП хватило бы выждать 1 период clk (12,5 нс в нашем случае), но контроллер Ethernet требует в этом месте хотя бы 50 нс - это уже 4 периода clk, и примерно соответствует одному периоду SCK (там макс. частота заявлена 14 МГц, т.е один период - 71 нс). Похоже, что это общепринятая практика - выжидать именно один период SCK, сделаем так и мы.

Рассмотрим код нашего "командоаппарата":
	localparam sIdle =  5'b00000;
	localparam sStart = 5'b01111;
	localparam sB0 =    5'b10000;
	localparam sB1 =    5'b10001;
	localparam sB2 =    5'b10010;
	localparam sB3 =    5'b10011;
	localparam sB14 =   5'b11110;
	localparam sB15 =   5'b11111; 

	wire [4:0] State;
	
	assign nCS = (~State[4])&(~State[3]);
	wire IsDataBit = State[4];	
	wire IsFinalBit;

	lpm_counter	counter (
				.clock (clk),
				.cnt_en (slow_ce),
				.sset (start),
				.q (State),
				.cout (IsFinalBit) );
	defparam
		counter.lpm_direction = "UP",
		counter.lpm_port_updown = "PORT_UNUSED",
		counter.lpm_type = "LPM_COUNTER",
		counter.lpm_width = 5,
		counter.lpm_svalue = 5'b01111;	


При инициализации ПЛИС (или по окончании предыдущей работы) мы сидим в состоянии sIdle (все нули). Только в этом состоянии nCS равен единице, т.е мы держим АЦП отключённой. Вместо условия nCS = (State == sIdle) мы воспользовались упрощённым: nCS = (~State[4])&(~State[3]), как всегда из нашей жадности. Поскольку из 32 возможных состояний, мы используем только 18, и лишь два из них имеют нулевой старший бит (это sIdle и sStart), а в середину мы никоим образом попасть не можем, то почему бы не упростить?

Когда старший бит счётчика равен единице, это означает - у нас уже работает SCK, мы вовсю принимаем и передаём данные.

Когда начинает приниматься последний бит, с выхода каскадирования счётчика cout (carry-out) приходит единица, этот провод мы назвали IsFinalBit.

Наконец, рассмотрим сам счётчик - мы опять поставили "библиотечный элемент", поскольку именно он гарантирует минимальное количество задействованных логических элементов (при реализации на "чистом верилоге" он иногда в упор не видит счётчика, особенно, если там "накручено" логики управления). Счётчик прибавляет единичку каждый раз, когда приходит импульс slow_ce (о нём чуть ниже). Пока мы находимся в режиме ожидания, этот импульс прийти не может, поэтому мы стоим на месте. Когда приходит импульс start, он выступает как вход синхронной загрузки, и мы переходим в состояние sStart. Досчитав до максимального значения 11111, счётчик спокойненько переходит в нулевое состояние sIdle, и там застревает до следующего импульса запуска, поскольку импульсы slow_ce опять перестают идти.

Теперь разберём код для формирования сигнала SCK и управляющих импульсов:
reg rSCK = 1'b0;
assign SCK = rSCK & IsDataBit;

wire slow_ce = ce & rSCK; 
wire MISO_ce = ce & (~rSCK);

assign finished = IsFinalBit & slow_ce;	
wire EnableShiftReg = slow_ce | start; 



always @(posedge clk) begin
		rSCK <= nCS? 1'b0 : ce? ~rSCK : rSCK;
end


Итак, у нас есть регистр rSCK, он остаётся нулевым, пока мы в режиме ожидания, а затем начинает переключать своё значение по каждому импульсу ce (как мы помним, они должны иметь удвоенную частоту SCK). Тем самым, формируется меандр.

Но первый период этого меандра мы не хотим выдавать на выход, поэтому "маскируем" его со значением IsDataBit. В итоге, мы имеем ровно 16 импульсов, поданных на провод SCLK.

Когда приходит импульс ce, и при этом rSCK = 1, у нас формируется импульс slow_ce, и только он сформировался, как rSCK переключается в ноль. Таким образом, мы формируем импульсы по отрицательному фронту SCK. Именно в этот момент мы сдвигаем данные и переключаем состояния.

Если же по приходу импульса ce у нас rSCK = 0, то взамен формируется импульс MISO_ce - это происходит по положительному фронту SCK - это наиболее благоприятный момент для "защёлкивания" сигнала с провода MISO.

Сигнал finished формируется на последнем падающем фронте SCK - вполне удобный момент.

Наконец, сигнал EnableShiftReg показывает все моменты, когда сдвиговый регистр должен "что-то делать", а не просто тупо хранить данные, внесённые ранее. Введён также из жадности. Как показал опыт, выражение вида

Q[11:1] <= start? {4'bxxx0, Ain, 5'bxxxxx}: slow_ce? Q[10:0] : Q[11:1];


(защёлкнуть входные данные по сигналу start, сдвинуть данные влево по сигналу slow_ce, либо хранить "как есть") синтезируется в безумные 22 логически элемента (LE), если не больше (т.е по 2 элемента на каждый регистр!). Объяснение примерно такое: на вход каждого регистра мы должны подвести провода start, Ain (данные, которые надо защёлкнуть по start), slow_ce, а также значение "себя любимого" и своего соседа справа - итого выходит 5 входов, тогда как наш ГФ имеет только 4 входа!



Но каждый логический элемент можно сконфигурировать так, чтобы 1-й вход также управлял ena - входом разрешения работы регистра. В таком случае, нам не нужно выход регистра соединять с его же собственным входом - достаточно будет просто "отключить" его, и он сохранит своё значение.

К сожалению, компилятор не очень умён, и иногда такую возможность "в упор не видит". Вот и приходится его буквально тыкать носом:
if (EnableShiftReg)
  Q[11:1] <= start? {4'bxxx0, Ain, 5'bxxxxx}: Q[10:0];


Вот теперь он всё понимает и сокращает число LE вдвое, до 11 - теоретического минимума.

Наконец, разберём код работы со сдвиговым регистром, который непосредственно обменивается данными с АЦП:
assign MOSI = Q[11];
assign MISO = nCS? 1'b0 : 1'bz;

always @(posedge clk) begin
  if (EnableShiftReg)
    Q[11:1] <= start? {4'bxxx0, Ain, 5'bxxxxx}: Q[10:0];
  if (MISO_ce)
    Q[0] <= MISO;


Как видно, выход MOSI попросту соединён со старшим битом сдвигового регистра (как мы и обещали).
Во второй строке мы "притягиваем" на землю вход MISO, чтобы он не болтался в воздухе. Слово assign как бы и означает, что мы задаём свойство выходного "драйвера" для данного вывода. В данном случае он либо коммутирует на землю, либо стоит в сторонке и ничего не делает.

Регистр мы "поделили" на младший бит и всё остальное. В младший бит записывается значение с провода MISO по положительному фронту SCK, тогда как сдвиг производится по отрицательному фронту.

При поступлении сигнала start мы "защёлкиваем" в регистр исходные 2 бита (выбор канала), но мы их поместили на единицу правее, чем "следовало бы", поскольку мы не стали запрещать работу регистра на сдвиг, пока мы находимся в состоянии sStart. Я надеялся, что это упростит управляющую логику - так и есть, но на количество требуемых логических элементов это не повлияло.

И остаётся рассмотреть логику формирования выхода Chan:
reg [1:0] NextChan = 2'b00;
always @(posedge clk) begin
  if (start) begin
    Chan <= NextChan;
    NextChan <= Ain;
  end
end


Данный узел вообще не связан со всем остальным! Всё, что он делает - "имитирует" задержку АЦП на один цикл измерений, чтобы у нас на выходе Chan оказался бы тот же самый канал, который использовала АЦП.

К примеру, при подаче питания АЦП "выбирает" нулевой канал, и нулевым же значением мы инициализируем NextChan. Допустим, при первом запуске мы задали оцифровку 1-го канала (Ain = 1). При подаче импульса start, в регистр NextChan попадёт единичка, а в Chan ("на выход") придёт нолик, поэтому по окончании первого цикла оцифровки мы будем знать, что получили значение с нулевого канала!

Наконец, собираем всё воедино:
//специализированный модуль SPI для микросхемки ADC124s051. 
//подаём номер канала, с которого хотим считать, и одиночн. импульс start
//в нужный момент на выходе Q появится 12-битное оцифр. значение, на finished придёт одиночный импульс,
//означ: можно забирать! а в Chan будет указано, с какого канала сняты эти данные
// (позволит сразу запихнуть куда надо в оперативку, к примеру)
// ce - импульсы удвоенной частоты, на которой мы хотим производить оцифровку. Рекомендуется от 3,2 до 8 МГц (т.е на ce - от 6,4 до 16 МГц),
//иначе данные будут существенно искажены (утечки в УВХ и пр)
//без фанатизма - пущай отключается целиком, когда одна посылка окончена

module SPI_for_ADC124s051 (input clk,
                           input ce,
                           input [1:0] Ain,
                           input start,
                           inout MISO,
                           output nCS,
                           output SCK,
                           output MOSI,
                           output reg [11:0] Q = 1'b0,
                           output finished,
                           output reg [1:0] Chan = 2'b00);
  
  localparam sIdle =  5'b00000;
  localparam sStart = 5'b01111;
  localparam sB0 =    5'b10000;
  localparam sB1 =    5'b10001;
  localparam sB2 =    5'b10010;
  localparam sB3 =    5'b10011;
  localparam sB14 =   5'b11110;
  localparam sB15 =   5'b11111; 

  wire [4:0] State;
	
  assign nCS = (~State[4])&(~State[3]); //единица только в сост. idle
	 //благодаря "прорехе", можно не проверять все 5 бит
  wire IsDataBit = State[4]; //специально так расположили
		
  wire IsFinalBit; //он "прикрепляется" к carry-out счётчика, там такая схемотехника уже есть!
	
  lpm_counter	counter (
				.clock (clk),
				.cnt_en (slow_ce),
				.sset (start),
				.q (State),
				.cout (IsFinalBit) );
  defparam
    counter.lpm_direction = "UP",
    counter.lpm_port_updown = "PORT_UNUSED",
    counter.lpm_type = "LPM_COUNTER",
    counter.lpm_width = 5,
    counter.lpm_svalue = 5'b01111;													

    reg rSCK = 1'b0;

    wire slow_ce = ce & rSCK; //приходит в два раза реже, управляет сдвигом данных и переключением состояний
    wire MISO_ce = ce & (~rSCK); //моменты, когда мы должны "защёлкивать" значения входных битов
    //если уберём IsDataBit - во-первых, лишний раз защёлкнется на старте, но нам плевать в данном случае (данные не потеряем),
    //а также будет щёлкать непрерывно во время ожидания. Тоже пофиг, по большому счёту - мы уже свои данные снимем!
	
    wire EnableShiftReg = slow_ce | start; //at final bit we probably loaded new portion of data. Shouldn't shift it already!
    //отсюда можно убрать IsDataBit, в Idle работать не будет всё равно (slow_ce=0), а на start лишний раз сдвинется
    //но тогда Ain надо заложить на 1 правее...

  assign MOSI = Q[11];
  assign MISO = nCS? 1'b0 : 1'bz; //sinking MISO when chip is not active. NO DANGLING WIRES!!!	
  assign finished = IsFinalBit & slow_ce;
  assign SCK = rSCK & IsDataBit;

  reg [1:0] NextChan = 2'b00;
		
  always @(posedge clk) begin
    rSCK <= nCS? 1'b0 : ce? ~rSCK : rSCK; //как запустились - начинает тикать, при возвращении в режим ожидания - замолкает.
    if (EnableShiftReg)
      Q[11:1] <= start? {4'bxxx0, Ain, 5'bxxxxx}: Q[10:0];
    if (MISO_ce)
      Q[0] <= MISO;
    if (start) begin
      Chan <= NextChan;
      NextChan <= Ain;
    end
  end
endmodule


Компиляция данного модуля даёт 31 логический элемент - весьма и весьма компактно! Необходимый минимум:
- 12 регистров для сдвигового регистра,
- 5 регистров для счётчика состояний,
- 1 регистр для rSCK,
- 4 регистра для Chan и NextChan,
- 4 генератора функций (не привязанных к регистрам) для finished, SCK, MOSI и nCS.

Это уже выходит 26 ЛЭ - меньше этого значения сделать попросту невозможно! Так что мы, как обычно, подобрались весьма близко, дальнейшая оптимизация потребовала бы, видимо, выполнить за компилятор его работу и распихать логические функции "вручную"! Мы маньяки, но не до такой степени.

Вот схема для проверки работы модуля:



И получающиеся осциллограммы (отклик от АЦП, MISO, мы здесь задали "ручками"):


Так оно выглядит "в железе":


(правда, название ролика показывает, что здесь я использую совсем другую АЦП - 8-битную быструю (до 100 МГц) AD9283, которая подключена к ПЛИС просто по 8 проводам, плюс тактовый (для запуска) и запрета работы, но потом я попробовал теми же светодиодами "помигать" с помощью ADC124s051 и особенной разницы не заметил, двоичный код он и в Африке двоичный! Так что снимать то же самое уж не стал...)


В следующем посте расскажу, как распихать показания 4-х каналов по разным ячейкам памяти (ну, это довольно тривиально), а также как преобразовать двоичный код в двоично-десятичный и сделать "аппаратный printf", который отправляет по UART форматированный текст. Как всегда, жадность будет зашкаливать!

Дело даже не в жадности. Просто кто-то разгадывает кроссворды, кто-то решает Судоку, а мне доставляет извращённое удовольствие кодировать эти "типовые задачи", на достаточно низком уровне, задействуя настолько мало ресурсов, насколько возможно.
Tags: ПЛИС, работа, странные девайсы
Subscribe

  • Так есть ли толк в ковариационной матрице?

    Задался этим вопросом применительно к своему прибору чуть более 2 недель назад. Рыл носом землю с попеременным успехом ( раз, два, три, четыре),…

  • Big Data, чтоб их ... (4)

    Наконец-то стряхнул пыль с компьютерной модели сближения, добавил в неё код, чтобы мы могли определить интересующие нас точки, и выписать…

  • Потёмкинская деревня - 2

    В ноябре 2020 года нужно было сделать скриншот несуществующей программы рабочего места под несуществующий прибор, чтобы добавить его в документацию.…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 2 comments