Теперь посмотрим реализацию на практике измерения температуры.
Проблема №1: у нас есть PDF с таблицей миливольт через градус, а нам надо таблицы код-от-холодногоспая и термозначение-от-кода.
Итак, сперва превратим PDF в текст с помощью pdftotext, затем пройдём по таблице, и будем превращать каждое значение миливольт таблицы в код (умножая на 1000 чтобы превратить в микровольты, затем на 104.8575875 для кода АЦП)? интерполировать значения между соседними точками, распечатывая значение для каждого кода в 65536.
Не забудем учесть, что в отрицательной части таблица указана по возрастанию модуля — а потому каждую отрицательную строку следует развернуть.
Когда будем проходить мимо чисел от 0..100°, аналогично поступим с кодом холодного спая, только запоминать будем каждые 32768 не температуру, а вычесленный код ацп.
Чтобы избавиться от знака, код АЦП для минимальной температуры с обратным знаком будет смещением, таким образом получим монотонную положительную таблицу.
Итак, мы получаем вот такой конвертер: convert.pl
Итак, мы получили .a51 инклуд, в котором объявлена константа ThermoBase — код, на который смещен ноль градусов в коде ацп, объявлена табличка ThermoTable, заданная через каждые 8000h кодов ацп, константа ThermoMax для проверки на перегрев, и таблица кодов термопары соответствующих холодному спаю ThermoExtTable через каждые 10000h кодов ацп термодатчика.
Обработчик прерывания от ацп будет крайне простым:
;************************************************************************** ; INTERRUPT from ADC ORG 0033H JBC DoExtReq, ADCExt ; Измеряли опору или термопару? ADCInt: MOV DataH, ADC0H ; Термопару сохраняем в DataH/M/L MOV DataM, ADC0M MOV DataL, ADC0L SETB ADCReady ; И ставим бит ADCReady. Бит ADCBusy сбросит основная программа CLR RDY0 RETI ADCExt: MOV ExtH, ADC0H ; Опору измеряем в ExtH/M/L MOV ExtM, ADC0M MOV ExtL, ADC0L CLR ADCBusy ; И очищаем бит ADCBusy (опора будет учтена для следующего измерения) CLR RDY0 RETI
В зависимости от состояния бита DoExtReq считываем показания АЦП либо в Data, либо в Ext.
Основная часть программы так же проста:
NoWD: JBC ADCReady, CalcThermo ; Измерили АЦП? JB ADCBusy, SeeCMD ; Игнорируем работу с АЦП, если он занят JBC NeedExtReq, InitExtReq JB NeedAutoReq, InitAutoReq SeeCMD: ; Прочая работа
– Если есть свежие показания АЦП — переходим к собственно вычислениям в CalcThermo
– Если показаний нет, ничего не делаем пока АЦП занят
– Если нет показания и АЦП свободен, проверим надо ли переизмерить показания холодного спая? (эти измерения можно проводить довольно редко, по сравнению с измерениями самой термопары. Я обновляю их раз в 2 сек, а термопару 5 раз в сек)
– Если нет показания, АЦП свободен, проверим не пора ли обновить температуру термопары.
Итак, само измерение холодного спая инициируется такой последовательностью:
InitExtReq: SETB ADCBusy ; АЦП занят SETB DoExtReq ; Измерение будет произведено холодного спая CLR ExtReady ; Нужно перевычислить холодный спай CLR RDY0 MOV ADCSF, #REQ_SF MOV ADC0CON1, #00100111b ; Измерение 0..1.28V, однополярно MOV ADC0CON2, #00001011b ; Измерение AIN3-AIN4 с внутренней опорой MOV ADCMODE, #00100100b ; Калибровка нуля MOV ADCMODE, #00100101b ; Калибровка максимума MOV ADCMODE, #00100010b ; Запросить 1 измерение на основном АЦП JMP MAINLOOP
Полностью аналогично производится инициация запроса показаний термопары:
InitAutoReq: SETB ADCBusy CLR RDY0 MOV ADCSF, #REQ_SF MOV ADC0CON1, #00000011b ; Измерение +-80mV биполярно MOV ADC0CON2, #00001010b ; Измерение AIN1-AIN2 в внутренней опорой MOV ADCMODE, #00100100b ; Калибровка нуля MOV ADCMODE, #00100101b ; Калибровка максимума MOV ADCMODE, #00100010b ; Запросить 1 измерение на основном АЦП JMP MAINLOOP
Итак, произошло измерение термопары, следовательно пора получить измеряемую температуру. Для этого нам понадобится еще эквивалент холодного спая, а потому, преамбула будет такой:
CalcThermo: MOV DevErr, #0 MOV DPCON, #00010001b ; Выберем второй дптр с автоинкрементом после MOVC/MOVX ORL PSW, #18h ; Выберем банк 3 JNB ExtReady, CalcExt JMP CalcMain
во-1х, при вычислении нам придётся часто менять DPTR и считывать табличные значения, поэтому имеет смысл переключиться на второй DPTR.
Использование автоинкремента особенно удобно при записи запись у 8051 идёт только по DPTR, и потому вместо
MOVX @DPTR, ... INC DPTR MOVX @DPTR, ...получается просто
MOVX @DPTR, ... MOVX @DPTR, ...Однако же при чтении из памяти данных это не так удобно, вместо
CLR A MOVC A, @A+DPTR ... MOV A, #1 MOVC A, @A+DPTRпри автоинкременте всё равно приходится каждый раз очищать аккумулятор:
CLR A MOVC A, @A+DPTR ... CLR A MOVC A, @A+DPTRоднако, во-1х CLR A занимает на 1 байт меньше, чем MOV, и во-2х сложнее ошибиться.
Итак, если у нас изменились показания холодного спая (ExtReady очищен), то нужно перевычислить эквивалент холодного спая.
Сделаем это прямо «в лоб»: отнимем 500мв, проверим что результат в нашей таблице:
CalcExt: ; Вычислим Ext-646303h CLR C MOV A, ExtL SUBB A, #03h MOV ExtL, A MOV A, ExtM SUBB A, #63h MOV ExtM, A MOV A, ExtH SUBB A, #64h MOV ExtH, A MOV DevErr, #1 JC ThError ; Если меньше 500mV -- температура отрицательная, ошибка (?) ; A = ExtH = точка входа в таблицу хол.спая CJNE A, #83H, IsOver83H IsOver83H: MOV DevErr, #2 JNC ThError ; Если >= 83h -- выход, окружающая среда больше 65°
Итак, теперь у нас в Ext лежит сдвинутый код термодатчика холодного спая, из которых ExtH = указатель по таблице (он же сейчас в ACC) и ExtM/ExtL пропорциональность от 0 до 65536. Найдём точку входа в таблицу:
; Таблица до 83h, по 3 байта => 83h*3=189h, то есть 1 бит переноса может быть CLR C RLC A MOV F0, C ADD A, ExtH ORL C, F0 ; A = младший байт, C = перенос. Это надо сложить с ThermoExtTable и положить в DPTR MOV DPH, #HIGH(ThermoExtTable) JNC NoIncDPH1 INC DPH NoIncDPH1: ADD A, #LOW(ThermoExtTable) MOV DPL, A JNC NoIncDPH2 INC DPH CLR C NoIncDPH2: ; DPTR = указатель на таблицу соответствия холодному спаю
(обратите внимание, может быть только один бит переноса, но операций сдвиг/сложение две, поэтому временно один перенос сохраняем в F0).
Настроив DPTR, считываем из таблицы начальную точку диапазона:
CLR A
MOVC A, @A+DPTR
MOV ExtTL, A
CLR A
MOVC A, @A+DPTR
MOV ExtTM, A
CLR A
MOVC A, @A+DPTR
MOV ExtTH, A ; ExtT=начальная точка интервалаЗатем считываем следующую точку и сразу вычисляем разность — длину интервала
CLR A
MOVC A, @A+DPTR
SUBB A, ExtTL ; C был очищен выше
MOV R0, A
CLR A
MOVC A, @A+DPTR
SUBB A, ExtTM
MOV R1, A ; R1:R0 = разность между верхней и нижней точками интервала
; Теперь в ExtM:ExtL находится разница внутри диапазона, в R1:R0 находится длина диапазонаОбратите внимание, что так как длина диапазона влезает в 2 байта, достаточно было считать только два байта.
Теперь нужно вычислить ExtT=ExtT+(ExtM:ExtL*R1:R0/65536)
Вычислим ExtM:ExtL*R1:R0: 16bit+16bit=32bit, нас интересуют только старшие 17бит (17й бит = округление)
Сложим результат в R5:R4:R3:R2 (R2 вообще не считаем)
MOV B, ExtM ; A=R1 на текущий момент MUL AB ; B:A = R1*ExtM => результат в R5:R4 MOV R5, B MOV R4, A MOV A, R1 MOV B, ExtL MUL AB ; B:A = R1*ExtL => Результат в R4:R3 (к R4 прибавляем) MOV R3, A MOV A, B ADD A, R4 MOV R4, A JNC NoOv1 INC R5 ; сохраним перенос если был NoOv1: MOV A, R0 MOV B, ExtM MUL AB ; B:A = R0*ExtM => результат к R4:R3 ADD A, R3 MOV R3, A MOV A, B ADDC A, R4 MOV R4, A JNC NoOv2 INC R5 ; сохраним перенос NoOv2: MOV A, R0 MOV B, ExtL MUL AB ; B:A = R0*ExtL => результат в R3:R2, R2 вообще игнорируем, у R3 проверяем только перенос и >128 MOV A, B ADD A, R3 JNC NoOv3 INC R4 CJNE R4, #0, NoOv3 INC R5 ; Если R4 стал равен нулю, то перенос ушел в R5 NoOv3: JNB ACC.7, NoOv4 ; проверим, не следует ли округлить вверх INC R4 CJNE R4, #0, NoOv4 INC R5 NoOv4: ; Теперь в R5:R4 интерполяционная добавка к текущему значению в ExtT MOV A, ExtTL ADD A, R4 MOV ExtTL, A MOV A, ExtTM ADDC A, R5 MOV ExtTM, A JNC NoOv5 INC ExtTH NoOv5: ; Теперь ExtT = показания холодного спая в табличных значениях термопары SETB ExtReady
Итак, теперь у нас есть эквивалент холодного спая и мы можем получить текущую температуру.
CalcMain: ; Вычитаем из кода пьедестал (код становится положительным), добавляем холодный спай и по таблице ; интерполируем температуру MOV A, DataH ADD A, #80H ; Переводим из 8000000 = 0 к 0=0 MOV R2, A ; Добавляем холодный спай MOV A, DataL ADD A, ExtTL MOV R0, A MOV A, DataM ADDC A, ExtTM MOV R1, A MOV A, R2 ADDC A, ExtTH MOV R2, A MOV DevErr, #3 ; Ошибка "ниже минимума" = "обрыв термопары" JB ACC.7, ErrorMin ; Ниже минимума таблицы? ; Теперь проверяем не вылезли ли за таблицу сверху CJNE R2, #ThermoMaxH, ChkMax CJNE R1, #ThermoMaxM, ChkMax CJNE R0, #ThermoMaxL, ChkMax ChkErr: MOV DevErr, #4 ; Ошибка переполнение jThError: JMP ThError ErrorMin: MOV ThermoH, #0FFh MOV ThermoL, #0FFh AJMP ThError ChkMax: JNC ChkErr ; 0< =R2:R1:R0<ThermoMax = Код с АЦП с холодным спаем и за вычетом базы
Итак, мы получили код, таблица через 32768, так что если мы R2:R1:R0 умножим на два, мы получим в CY:R2 указатель по таблице, а в R1:R0 пропорцию из диапазона 0..65536.
CalcFind: ; Выбрать из таблицы код R2:R1:R0 CLR C MOV A, R0 RLC A MOV R0, A MOV A, R1 RLC A MOV R1, A MOV A, R2 RLC A ; A = индекс таблицы, от 0..С2, и CY=0 RLC A ; A=указатель таблицы, C=перенос MOV DPH, #HIGH(ThermoTable) JNC NIncDPH INC DPH NIncDPH: ADD A, #LOW(ThermoTable) MOV DPL, A JNC NIncDPH2 INC DPH NIncDPH2: ; DPTR = ThermoTable+(R2:R1:R0/32768)*2
Итак, теперь аналогично работе с таблицей термопары выберем первую точку и длину интервала:
CLR A
MOVC A, @A+DPTR
MOV R3, A
CLR A
MOVC A, @A+DPTR
MOV R4, A ; R4:R3 = код из таблицы
CLR C
CLR A
MOVC A, @A+DPTR
SUBB A, R3
MOV R5, A
CLR A
MOVC A, @A+DPTR
SUBB A, R4
MOV R6, A ; R6:R5 = разница между следущим и текущим кодомИ сдвинемся внутри интервала:
; Теперь в R6:R5 разница по таблице, в R1:R0 пропорция из диапазона 0..65536 ; Вычислим [ R6:R5*R1:R0 = ThermoH:ThermoL:Tmp1:Tmp2 ] / 32768, ; от Tmp1 нужен только старший бит (для определения округления) MOV B, R1 MUL AB ; R6*R1 нужно положить в ThermoH:ThermoL MOV ThermoH, B MOV THermoL, A MOV B, R1 MOV A, R5 MUL AB ; R5*R1 складываем в ThermoL:Tmp1 MOV Tmp1, A MOV A, B ADD A, ThermoL MOV ThermoL, A JNC ResNoO1 INC ThermoH ; Учтем перенос ResNoO1: MOV A, R6 MOV B, R0 MUL AB ; R6*R0 складываем в ThermoL:Tmp1 ADD A, Tmp1 MOV Tmp1, A MOV A, ThermoL ADDC A, B MOV ThermoL, A JNC ResNoO2 INC ThermoH ResNoO2: MOV A, R5 MOV B, R0 MUL AB ; R5*R0 складывается в Tmp1:Tmp2, интересует только старший бит Tmp1 и перенос MOV A, B ADD A, Tmp1 JNC ResNoO5 XCH A, ThermoL INC A JNZ ResNoO4 INC ThermoH ResNoO4: XCH A, ThermoL ResNoO5: ; Если ACC.7!=0, увеличим ThermoHL JNB ACC.7, ResNoRound INC ThermoL MOV A, ThermoL JNZ ResNoRound INC ThermoH ResNoRound: ; Теперь в ThermoH/ThermoL лежит добавка к текущему значению таблицы, добавим к ней R4:R3 MOV A, ThermoL ADD A, R3 MOV ThermoL, A MOV A, ThermoH ADDC A, R4 MOV ThermoH, A JNC NoThError ; Перебора тут быть не должно, ибо температура за 2048 градусов вылезать не должна MOV DevErr, #7 AJMP ThError NoThError: ; --- CalcEnd: ; Конец вычислений. В Thermo* лежит вычисленное значение
Итак, все наши вычисления завершились. Восстанавливаем состояние и переходим к дальнейшей части работы:
MOV DevErr, #0 CLR ADCBusy ; АЦП свободен ; Вернуться к обычной работе ANL PSW, #NOT 18h ; Выбрать банк 0 MOV DPCON, #0 ; Вернуться к основному DPTR JMP MAINLOOP ; Вернуться к основной программе
Вот так, весело и задорно, и довольно компактно, получаем искомую температуру с термопары :)