Работа с АЦП. Программные средства повышения точности

Распечатать

Вообще, сграбив сигнал в цифровую форму мы мы можем извращаться с ним как угодно. Методик цифровых фильтраций существует масса и все они основаны на сборе избыточной информации с последующим выделением сигнала. Я приведу для примера лишь один простейший способ — усреднение.

Суть усреднения в том, что у нас есть статичный (считаем, что за время измерения сигнал не меняется) сигнал к которому подмешан шум. Шум возникает изза работы транзисторов, из-за колебания опорного напряжения, помех, наведенных на сигнальные линии. Да от чего только он не возникает. Особенность шума в том что он, как правило, хаотичен. Так что во время нашего замера может меняться как в меньшую так и в большую сторону. Тут то мы его и прижучим.

Берем и снимаем не одно измерение, а сразу кучу. А потом берем по ним среднее арифметическое. Так как полезный сигнал у нас константен, то его составляющая такой и останется, как ее не усредняй, а вот шум изрядно приглушит. И чем больше выборок мы сделаем, тем сильней задавит шумовую составляющую. Западло этого метода очевидно — резко снижается скорость обработки. Так как вместо одной выборки нам приходится делать серию и объявлять ее как одну, но это неизбежное зло.

В качестве демонстрации метода я приведу пример усреднения. Программка простая, хватает 64 выборки, усредняет их и отправляет по UART. Сразу отмечу тот факт, что для эффективного подавления шума нужно чтобы частота выборок была ниже частоты всяких паразитных колебаний (вроде 50Гц наводок от сети) раза в два три, иначе у нас эти колебания благополучно пролезут как полезный сигнал. А еще число выборок нужно брать кратным двойке, чтобы можно было делить простым сдвигом. Впрочем, смотрите на код, там будет более понятно. Весь код я выкладывать не буду, только главный файл. Все инициализации АЦП и UART, а также ряд служебных процедурок я оставлю за кадром. Если интересно, то вы всегда можете скачать проект и посмотреть сами. Сбор числа у меня идет в прерывании от АЦП, а деление в прерывании по передаче. Так минимизируется число действий выполняемых процом. Хотя растягивание прерываний это не есть гуд. Но городить флаговую операционную систему мне тут впадлу, впрочем, дойдет и до нее время. А пока смотрите в код:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
		.include "m16def.inc"	; Используем ATMega16
		.include "define.asm"	; Наши все определения переменных тут
		.include "macro.asm"	; Все макросы у нас тут
		.include "vectors.asm"	; Все вектора прерываний спрятаны в этом файле
;=============================================================================			
 
		.ORG	INT_VECTORS_SIZE		
Reset:		OUTI 	SPL,low(RAMEND) 		
		OUTI	SPH,High(RAMEND)								
 
;=============================================================================
		.include "init.asm"		; Все инициализации тут.
;=============================================================================
Main:		SEI				; Разрешаем прерывания.
 
 
;Главный цикл пуст, весь экшн в прерываниях. 
 
 
		RJMP	Main
 
;=============================================================================
; Interrupts
;=============================================================================
; Прерывание от UART на передачу
UART_TX:	PUSH	R16		; Нычим регистры в стеке
		IN	R16,SREG
		PUSH	R16
 
 
; Сумму мы нажрали в прерывании по АЦП, а делить ее для усреднения будем уже тут
		CLC		; Сбрасываем флаг переноса
		ROR	SUMH	; И сдвигаем значение вправо сначала старший
		ROR	SUML	; потом младший. Через флаг переноса.
 
		CLC		; Всего нам надо 6 сдвигов 2^6=64
		ROR	SUMH	; Что будет эквивалентно делению на 64
		ROR	SUML
 
		CLC
		ROR	SUMH
		ROR	SUML
 
		CLC
		ROR	SUMH
		ROR	SUML
 
		CLC
		ROR	SUMH
		ROR	SUML
 
		CLC
		ROR	SUMH	; Теперь уменя готовое 10 байтное усредненное число
		ROR	SUML	; Но 10 байт мне нафиг не уперлись. Куда мне их совать?
				; Поэтому я отбрасываю еще два младших бита, превращая число
; в восьмибитное. Делаю я эту нехитрую операцию тем же самым сдвигом через перенос
 
		CLC
		ROR	SUMH
		ROR	SUML
 
		CLC
		ROR	SUMH
		ROR	SUML	; Все, теперь можно забыть про старший байт, он равен нулю
				; А весь жыр теперь находится в младшем байте SUML
 
 
 
		OUT	UDR,SUML ; Его то мы и отрыгиваем в UART. О переполнении буффера можно
				; Не париться, так как отправка идет исключительно по
 ; прерыванию, а значит пока байт не ушел никаких левых засылок не будет. 
 
		POP	R16		; Достаем из стека сныканое барахло.
		OUT	SREG,R16
		POP	R16
		RETI			; Выход из прерывания
 
 
;=============================================================
; Прерывание от UART на прием
UART_RX:	PUSH	R16		; Прячем в стек регистры
		IN	R16,SREG
		PUSH	R16
 
		IN	R16,UDR		; Берем пойманый байт
		CPI	R16,'s'		; И анализируем. Если S - значит стоп
		BREQ	StopADC
 
		CPI	R16,'r'		; Если r значит - запуск. 
		BREQ	RunADC	
 
; Запускаем АЦП, на непрерывное преобразование и включаем UART на передачу			
RunADC:		OUTI 	UCSRB,(1<<RXEN)|(1<<TXEN)|(1<<RXCIE)|(1<<TXCIE)
		OUTI	ADCSRA,(1<<ADEN)|(1<<ADIE)|(1<<ADSC)|(1<<ADFR)|(3<<ADPS0)
		OUTI	UDR,'R'
		RJMP	ExitRX
 
; А тут останавливаем и АЦП и выключаем UART иначе он болезный будет гадить 
; по прерыванию до тех пор пока питалово не кончится. 
StopADC:	OUTI	ADCSRA,(0<<ADEN)|(0<<ADIE)|(0<<ADSC)|(0<<ADFR)|(3<<ADPS0)
		OUTI 	UCSRB,(1<<RXEN)|(0<<TXEN)|(1<<RXCIE)|(0<<TXCIE)
 
ExitRX:		POP	R16			; Достаем из стека данные наши
		OUT	SREG,R16
		POP	R16
		RETI				; Выходим в главный цикл
 
 
;=============================================================================
; Прерывание от АЦП - обработка завершена
ADC_OK:		PUSH	R16			; Сохранить регистры в стеке
		PUSH	R17
		IN	R16,SREG	; Сохранить SREG 
		PUSH	R16
 
; Определяем ряд макросов для удобства использования
		.def	ADSH	= R15	; Старший байт суммы
   		.def	ADSL	= R14	; Младший байт суммы
		.def	ACT	= R13	; Счетчик усредняемых значений
		.def	SUMH	= R12	; Выходной регистр старший
		.def	SUML	= R11	; Выходной регистр младший
 
 
		IN	R16,ADCL	; Взять значение из АЦП
		IN	R17,ADCH
 
; Теперь пару R17:R16 надо просуммировать с их n-1 значением. При первой итерации
; тут ноль. При второй уже будет сумма с первой выборкой и так далее. 
 
 
		ADD	ADSL,R16	; Младший байт суммируем без переноса
		ADC	ADSH,R17	; Старший байт суммируем с учетом переноса
 
; В результате, в ADSH:ASSL будет накопленна сумма ADSH:ASSL(n) + ADSH:ASSL(n+1)
; Максимальное значение для 10битного АЦП - 1024, в то время как в 16разрядах
; Помещается 65536 значений, 65536/1024 = 64. Поэтому переменная АСТ должна быть
; Заранее инициализирована числом 64 			
 
		DEC	ACT	; Считаем сколько уже выборок мы сделали
		BRNE	Exit		; Если это не последнее слагаемое, то выход
 
		MOV	SUML,ADSL	; Сливаем выходное значение в регистр
		MOV	SUMH,ADSH	; младший и старший байты
 
		CLR	ADSL	; Потом мы очищаем регистры суммирования
		CLR	ADSH	; Чтобы новый результат был с нуля
 
		LDI	R16,64	; Заполняем счетчик новым числом
		MOV	ACT,R16
 
Exit:		POP	R16		; Достаем из стека все как было. 	
		OUT	SREG,R16
		POP	R17
		POP	R16
 
 
		RETI			; Возвращаемся.
Запись опубликована в рубрике AVR. Учебный курс с метками , , , , . Добавьте в закладки постоянную ссылку.

38 комментариев: Работа с АЦП. Программные средства повышения точности

  1. SWG говорит:

    Для медленно изменяющихся сигналов я шунтирую аналоговый вход керамическим конденсатором порядка 0,1 — 1,0 мкф. Поскольку измеряемое напряжение всегда подаю через ограничительный резистор или с делителя, шумы давятся достаточно хорошо. Кроме того, это уменьшает погрешность, возникающую при сопротивлении измерительной цепи более нескольких килоом (в момент измерения импеданс входа АЦП микроконтроллеров имеет емкостной характер).

    • Alb говорит:

      Зато усреднение по множеству точек при очень хорошо сделанной электронике позволяет «как бы увеличить разрядность АЦП». Например, если значение на входе между значениями A и A+1, то вероятности выпадения этих значений будут разные, и если сложить например N*4 значений а поделить на N, то младшие биты будут показывать значение входного сигнала более точно.
      А еще усреднение можно делать «скользящим окном» — имеется буфер A (скажем на 64 числа), в него по кольцу записываются новые значения АЦП, имеется сумма буфера S и имеется указатель в буфере i. Изначально буфер обнулен, сумма равна нулю. Каждое значение АЦП обрабатывается следующим образом:
      - из суммы S вычитаем элемент A[i]
      - считываем новое значение АЦП в adc
      - прибавляем его к сумме S
      - записываем его в A[i]
      - Прибавляем i и если оно превысило 63 — обнуляем i
      За значение, полученное с АЦП, принимаем S/64.
      Такой алгоритм во многом эквивалентен конденсатору — на каждое измерение АЦП мы получаем результат, но он усреднен с предыдущими. При этом не нужно каждый раз рассчитывать сумму и тратить на это время. Преимущества — можно усложнить алгоритм — если разница между полученным значением АЦП и полученным средним значением больше некоторого порога, можно заполнить весь массив новым значением, установить сумму как это значение*64 — и мы получим быструю реакцию на резкие изменения при стабильном показании, если на входе сигнал почти не меняется.

  2. _riko_ говорит:

    А в чем отличие команды MOV от LDI если по сути они оинаково пихают данные в регистр? Или это просто команда премещения байта из регистра в регистр (MOV R15,R14) ?

    • SWG говорит:

      MOV копирует содержимое одного регистра в другой, LDI загружает в регистр константу (непосредственно заданное число).
      Вообще, в AVR огромное количество команд отличается лишь типом операнда или методом адресации. Зато их много! (Атмел хвастается их количеством).
      А например, у PIC контроллеров команды более универсальны, и их всего 31-35. (И Микрочип считает, что это хорошо, т.к. их легче запомнить). В общем, каждому — свое. А мне так пофигу, я использую и те, и другие. Процессорам тем более пофигу, они работают только с теми 0 и 1, которые в них зашьет программатор.

  3. ArgusB говорит:

    А вообще, да. Шум в сигнале давится старым добрым RC фильтром, а данный метод хорош только усреднять шумы самого ацп. Вот я помню, пытался кое-что сделать на DS2450, так там младшие три бита — настоящий генератор случайных чисел. Плюнул, и придушил точность, а что ещё делать?

  4. cahbtexhuk говорит:

    Немножечко оффтопа.
    Хм, а почему все таки ассемблер? Просто есть нормальные Компиляторы на си, паскале, бэйсике, разных самописных экзотических языках. И платные, и бесплатные. И вполне компактный код дают потом. И уже хренова туча всяких макросов и стандартных процедур сведены к простм функциям.

    • DI HALT говорит:

      Чтобы было понятно как это работает на самом деле. А то я наблюдал такую картину:

      Человек пишет прогу под МК на Си, тоже с ацп. Я ему сказал про усреднение. Он берет усреднение по 100 точкам, а потом выполняет деление (ладно хоть не с плавающей точкой), а потом долго не может понять почему у него программа не влазит в ПЗУ. Ну хуле, что ты хотел дорогой — деление.

      А на асме когда пишешь сразу понятно откуда ноги торчат, как работают прерывания, как и куда девать переменные. И многие проблемы отпадают сами собой.

      А если не брать готовые процедуры (которые чуть более чем полностью являются избыточными, сделанными на все случаи жизни, в том числе гипотетические), то все программирование на том же си сводится к пинанию байтов по служебным регистрам.

      Мне, например, на ассемблере это делать в разы проще чем на сях. Да и ответственные проекты тоже предпочитаю писать на асме. чтобы проц шел четко по моим командам и я не получил потом задницу в самом неподходящем месте изза какого нибудь срыва стека.

    • DI HALT говорит:

      А еще я жадный, если программу можно уменьшить на пару байт я это постараюсь сделать. Зато у меня никогда не возникает проблем с нехваткой места на кристалле.

      • ArgusB говорит:

        Сэр DI HLT, это как понимать? В смысле, ваш крайний ответ. Вы что, ОТМАЗЫВАЕТЕСЬ ???? Это всё равно, если бы вы в совершенстве знали итальянский, по выходным бы летали, чтобы в Ла-Скала послушать Верди в оригинале, и вас бы упрекали в незнании сурджика такжикских гастарбайтеров!!! «Насяйника, три клёк смент ната, тааа.» Как вам не стыдно! Наоборот, гнать надо лентяев, которые не удосужились изучить принципы работы ввереной им мат.части.
        Пыщь!,пыщь отсюда со своим птичьим языком. Чем это они смогут удивить мир — волшебным умением программировать, или знанием алгоритмов? Нет, они способны удивить мир лишь хорошо затверженными баснями про кросс-платформенную переносимость. Переносимость? Такого зверя нет в природе, а если и есть, то он подобен обезьяне-ленивцу, что день-деньской висит на ветке головой вниз (c)Р. Бредбери.
        А вы им ешё потакаете….

      • Gandalf говорит:

        Раз речь пошла за минимизацию кода, не будет ли проще код

        CLC
        ROR SUMH
        ROR SUML

        заменить на

        LSR SUMH
        ROR SUML

        2 команды против 3 все таки=)))

  5. Alb говорит:

    Ну вообще «наезд на наш подъезд»! Ассемблер это конечно хорошо, но более-менее серьезный проект на нем писать слишком тяжело. Там уже не до экономии памяти — пойди через год вспомни о чем тут речь шла и где нужно изменить чтобы «вот тут буковка изменилась и температура в цельсиях стала показываться, а не в кельвинах». Так что основы работы контроллера лучше объяснять на asm, а вот принципы построения программ — на Си. Ибо тут важно _усреднение_, а не _как_его_написать_на_асме_.

    • DI HALT говорит:

      А где ты тут видишь сложный проект? ;))))) Я щас, кстати, мозги робота развожу (точнее наверное это все равно будет мозжечок, сам мозг наверное будет все же на пропеллере или арме) на АТМЕГА128 вот там буду писать на сях.

      А тут я сделал так, чтобы показать, как усреднение делается, да еще простым сдвигом. Практически нахаляву.

  6. mtr говорит:

    Господа, простите, что я встреваю, но не могу молчать
    Уважаемый DI Halt, что такое «частота шума»? До сих пор думал, что частота бывает только у периодических сигналов… Если вы имели в виду верхнюю частоту спектра шума, то это плохо. Будет наложение (Котельников и все такое) и весь оверсэмплинг коту под хвост.
    Оверсэмплинг действительно хорошо помогает увеличить точность, но при правильном применении и не совсем программно, например в сигма-дельта ацп/цап.
    Вообще говоря, железо софт не заменит. В присутствии шума, если ширина его спектра больше или равна частоте дискретизации, весь спектр цифрового сигнала исказится, даже постоянная составляющая.

    • DI HALT говорит:

      Да, я там неверно выразился, имя под шумом всякие левые наводки, которые обычно присутствуют, например сетевые 50Гц.

      Железо софт не заменит, я это не отрицаю. Но зачастую, для простых случаев, элементарный софт, вроде усредненки, вполне себе спасает ситуацию.

  7. хм, вообще-то существуют аналоговые защелки. Может они здесь будут полезны?

    • SWG говорит:

      Нет, они сохраняют значение, существовавшее в момент выборки, а в условиях помех оно может оказаться каким угодно. Больше подходит интегратор, он сглаживает случайные броски.
      Кроме того, в микроконтроллерах уже есть устройство выборки/хранения, — в начале цикла измерения заряжается конденсатор в АЦП от входа, затем отключается, и начинается его разряд калиброванными порциями.

  8. KeFiR говорит:

    Если команду ror при сдвиге старшего байта заменить на lsr, то можно выкинуть очистку флага переноса.

  9. BuHTuk говорит:

    8 раз:
    CLC
    ROR SUMH
    ROR SUML
    ——
    OUT UDR,SUML

    не равносильно ли просто

    OUT UDR,SUMH ?

    и еще
    CPI R16,’s’
    BREQ StopADC
    CPI R16,’r’
    BREQ RunADC

    RunADC: …….

    Последний BREQ не делает ничего =)

    • ZeroQu говорит:

      Насчет первой части — похоже что результат тот же, но DI видимо просто хотел более наглядно показать, что именно делается и зачем (сдвиг сначала на 6 потом еще на 2).
      По поводу последнего BREQ — да, формально он излишен. Однако я, например, так пишу потому, что так легче работать с процедуами в дальнейшем. Если понадобиться добавить потом перед RunADC:… какой-нить код, то вполне можно порушить логику. Лучше сразу писать с явным условным переходом там, где надо.

  10. dima_m говорит:

    А для чего в каждом прерывании в самом начале стоят такие команды?
    PUSH R16
    PUSH R17
    IN R16,SREG
    PUSH R16

  11. http://shura.luberetsky.ru/ говорит:

    Ошибочку заметил: в 54 строчке кода написано «ROR SUMH ; Теперь уменя готовое 10 байтное усредненное число». Не 10-байтное, а, все-таки 10-битное.

  12. gp1 говорит:

    Наконец-то добрался до чтения учебного курса, есть много вкусного, но ИМХО я бы SEI все таки вынес за пределы бесконечного цикла 8-)

  13. Freerider говорит:

    А как вам такое решение
    star_adc_data-старое значение рез-тата АЦП
    adc_data-новое
    urov_pomex-уровень помех в мВ
    dU-разница показании
    if (star_adc_data!=adc_data)//Если рез-тат изменился то выполняем
    {
    if((adc_data>star_adc_data-urov_pomex)&&(adc_data<star_adc_data+urov_pomex))//Проверку
    // Если рез-тат adc_data лежит пределах star_adc_data+urov_pomexstar_adc_data-urov_pomex
    {adc_data=star_adc_data;dU=0;}//То мы записываем в него старое значение
    else
    { if(adc_data>star_adc_data){dU=adc_data-star_adc_data;};
    if(adc_data<star_adc_data) {dU=star_adc_data-adc_data;};
    star_adc_data=adc_data;
    };
    };
    Например star_adc_data=12
    новый рез-тат adc_data изменился за счет помехи на 3 мВ adc_data=15
    urov_pomex=4; 12-4<15<12+4
    поэтому adc_data примет значение star_adc_data и на выв.экране (lcd или usart comp) не так будут прыгать цифры.

  14. dundich говорит:

    А как можно на асме разделить два переменных числа??? я хотел бы разделить на результат преобразования другого канала АЦП….
    Здесь же сдвиг разрядов не поможет!!!

  15. dundich говорит:

    Что в студии не могу найти, как создать файл с расширением .inc. Подскажите, пожалуйста! А еще есть ли какая-нибудь разница, в файле с каким расширеним будут храниться макросы, то ли в *.inc, то ли в *.asm ?

  16. Flint говорит:

    В примере суммируются 64 значения, так как для двухбайтной суммы большее количество даст переполнение. Подскажите, как суммировать большее количество результатов АЦП, если результат суммы будет храниться уже в четырех байтах. А то у меня с математикой и работой с флагами при этом не очень :)
    Попробывал суммировать не 64 значения, а 32, а потом усреднять — результат стал гораздо лучше, чем при 64 выборках. Это даже при 10-ти битном конечном результате.

  17. Ass.Kovalev говорит:

    В некоторых случаях удобно использовать рекурсивную фильтрацию. Позволяет строить как вч, так и нч фильтры с достаточно высоким быстродействием.
    Имхо, в некоторых случаях даже оптимальнее и проще, чем пакетная обработка.

    Есть группа коэффициентов фильтра (Cx, Cy), есть группа значений аккумулятора (Ay) (в простейшем случае это последний вычисленный отсчет или ноль в состоянии, когда данные еще не получены).
    Когда приходит значение с ацп (Ax), вычисляется новый отсчет по формуле
    Ay’ = Ax * Cx + Ay * Cy
    Меняя коэффициенты, можно отсеивать ненужные помехи в различных частях спектра сигнала.
    Для полосового фильтра, очевидно, самое простое — скаскадировать два таких фильтра с разными параметрами
    Расчет таких фильтров описан много где, сейчас примера книжки под рукой нет, вечером могу скинуть

Добавить комментарий