Текст
                    Электроника
Source
text file
Binary
Machine
Language
ARDUINO HA ASSEMBLER
На распутье
Вряд ли программы промышленных автономных устройств составлены на платформе
Arduino IDE. Где все спрятано в громоздкие тяжеловесные библиотеки, а простые
коды (скетчи) занимают в редакторе несколько десятков строк, делая работу в
этой среде комфортной и не требующей особых усилий. Уточню сразу - дальше
речь о выборе языка программирования между Ардуино, Си или Ассемблером. "Язык
Ардуино"- это сленг для краткости. Нет такого языка программирования, это
"Arduino IDE — интегрированная среда разработки для Windows, MacOS и Linux,
разработанная на Си и С ++".
Почему многие не любят Arduino? Ниже, для наглядности, картинка оттуда с
кодом «мигалки».
'оз forGeekTimes2 | Arduino 1.6.7
©
Файл Правка Скетч Инструменты Помощь
©О ПЕЭЕЗ
-;р I ,1 ■
ъ (13,
о П
- il'J'J'JI ;
(1000);
|^' forGeekTimesl | Arduino 1,6.7
О I ОО ШНН
■аашвввв
<avr л.о . л:
<util ■■':-.■=_
DDR3 - (1 « 5}
FCRT3 i=
_delay_ras
FCR73 -
delav ms
1 <■: 5};
1000);


Пример слева написан в платформе Arduino IDE, а справа — работа непосредст- венно с регистрами. Скетч выглядит несколько компактней, чем та же «мигалка», но с использованием регистров. На изображении ниже — компиляция кода «мигалки» на Ассемблере. Как видно, былая компактность испарилась - количество строк в 3 раза больше, чем в Ар- дуино. И u;5er4t<<r*-E'E'K.stionl ■ uh-r>e<l>.4^,; |Лг)ттг:>'г*-.- -3, *<-.:« *.'.:,л -- у J- ^ г.'\ *> *> к -. ч *-, -*~- w^v i '*■-• 1 nil! SPII, 'U»j + 1 1 LDI 416, LOH(RAMEND) 1 ом Г S?l , 416 1 MA IN: 1 LIU !(К., 0x H- 1 i:i.l| IJlbX f 41 о 1 '.ЧАС. < ; 1 OJI-' 416 1 OUT -'04R , 416 1 •: Д1 1 DM AY 1 4 :(•':' ;jAC < 1 DELAY; 1 1 IJ1 4 1 7 , 100 1 НИ !;•••■: МП R]H, /l-S 1 LOO "'2; LDI 419, 2У_> 1 LOU21: DK RV-* 1 --:*<:'if 100-4 1 f.Hi. ;U8 1 i:;4*H LOU P 2 1 и f ■: 417 1 -.-v-if- цнич 1 4L 1 ?;Л.Г::;\г'\,Н..-":'.':':.::: :'.:t:':::V/:::::;::::";:;':::;: :\:"::t Итак, с двух картинок выше видно - размер памяти, занимаемой в контроллере кодом «мигалки» одним светодиодом, написанным в платформе Ардуино, составляет 1030 байт, на Си - 176 байт, на Ассемблере - 42 байта. Теперь взглянем на более сложный код. Поскольку в своих проектах использую модуль давления-температуры ВМР280, составил код барометра-термометра на Си, чтобы заодно была какая-то польза. h На распутье - Ардуино, Си или Ассемблер? 7 #include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> #include ffbmpl80/bmpl80. c" #include "uart.c" #include <stdio.h> #include <stdlib.h> #include <string.h> #include "nokia/nokia5110.h" int main(void) { serial_init () ; DDRD |= (1 « 7); //pin 13, atmega328p PORTD &= ~(1 « 7);
nokia_lcd_init () ; while (1) { init_sensor (bmpl80_mode_0) ; calculate(); PORTD |= (1 « 7); _delay_ms(100); printf("Temperature:%.2fС,Pressure:%.2f Pa,\n",(float)bmp_180.temperature/ 10,(float)bmp_180.pressure); nokia_lcd_clear () ; nokia_lcd_write_string("789",l); nokia_lcd_set_cursor(0, 10); nokia_lcd_write_string("22.2", 3); nokia_lcd_render(); _delay_ms(2000); PORTD &= -(1 « 7); _delay_ms(100); int a=54325; char buffer[20]; itoa(a,buffer,2); // here 2 means binary printf("Binary value = %s\n", buffer); itoa(a,buffer,10); // here 10 means decimal printf("Decimal value = %s\n", buffer); itoa(a,buffer,16); // here 16 means Hexadecimal printf("Hexadecimal value = %s\n", buffer); } return 0; FiH u« v»«vk Ten Diuqr Ci*n-i Oteuq Library Т|гт^:Ыя V,4l»m Htip di АПм^я -► + < н с о 11 о О) л ш W J. 1 оо U.'TTCo. nr-; РЭ'ЛлХНРСКТ»? РЭ1ТЛ" ЮГ»РС Г Л О РО*ТО»СКРСГ/ГЯ] РЭГ.«.ЫРСГ4Г23 p*i V3 аог^лрс tin PB^CSC » ЛТП 1 *;?Я* ТОТГ ЭОСаГГЛДРС Г Л7 РС1ЛСС«ХГЛЭ PC:iAX:yPCJHTr Ok R? ь ., • -1 » J г L г * —^ — ms ^^^^^^^^H CTS> ншвша вд из 744 №,Ш 21.7С §5SsseS§ ¥" JL В проект входят следующие компоненты: контроллер ATMEGA328P, модуль давле- ния-температуры ВМР180 и дисплей Nokia 3110. ATMEGA328P принимает инфу с дат- чика ВМР180 и после преобразований отображает ее на дисплее Nokia 3110, затем спит. Сон задается сторожевым таймером Watchdog. Проект собирается в Atmel Studio 7 и эмулируется в Proteus 8 Pro. Этот проект Atmel Studio был создан
для отладки кода в Proteus*е. В библиотеке Proteus 8 Pro модуля ВМР280 нет, поэтому пришлось составить код с включением ВМР180. Светодиод в коде — для наглядности, чтобы придать динамику статичной картинке. Ниже — электрическая схема устройства. При монтаже схемы обращайте внимание на функциональное назначение выводов контроллера и модулей. Подключение квар- ца — XTAL1, XTAL2 (ATMEGA328P) . Уточню, схему барометра-термометра на ВМР180 я **в железе** не собирал, поэтому тут могут проявиться проблемы, которые не видны при эмуляции в Proteus * е. i г~~з F-C O^'xD/pi": 1Г Л" 1R F01/T.XD/PCIWT17 р-С'2''1ГчГГГ1.'РС1ГЛ"18 FO-У1Г-Л" 1.'ОС 2&РС IГ-Г19 pi:4,'Tr^xr;K,'Pcirrjo FDST1/0C OB/PC INT21 PC tv.*! Ni>:jC0A.'"PC INT22 F07/AJN1.'P:iNT22 AREF AVri'j ATMEOA32tP РВПЛСР'/С1.И0/РС NTO -'BVCC'A/PC NT1 Pe2/S5.-vX*B/PCNT2 PB3,'Mos;i:'C2A;Fcir.fT3 P64y\1iS0.'PONT4 РБ5/ЗСК,РС1ГГ5 РВ£ЛС6С 1VXTAL1 'PC INT 6 PB7/TOSC 2-' X TAL2/FC INT7 -'COVADCCvPCir-rS aC1'ADC1/PCIN"9 PC2,'AZ-C2.»PCINT,0 PC?..'ACC^PCINT'1 РС1'А1С.Д/50А/РС1Гч1Т12 FCfv"ADi^/5CUFCir4T13 PCn;RFsFT,'pLir-ru 19 NOKAi310 U2 Л VJOIC BMP 180 ♦3 3V Ниже — код этого же барометра-термометра в платформе Arduino IDE. Естест- венно, с другими библиотеками. /' На распутье - Ардуино, Си или Ассемблер? 7 #include <SPI.h> #include <LowPower.h> #include <SimpleTimer.h> #include <Adafruit_BMP280.h> #include <Adafruit_GFX.h> //https:esp8266.rU/forum/threads/esp8266-5110-nokia-lcd.1143/#post-16942 #include <Adafruit_PCD8544.h> //https:esp8266.rU/forum/threads/esp8266-5110-nokia-lcd.1143/#post-16942 Adafruit_BMP280 bmp280; float Press, Tin; //давление, температура Adafruit PCD8544 display = Adafruit PCD8544(5, 7, 6); void setup() {
Serial.begin(9600); di splay.begin() ; // display. clearDisplay () ; display.setContrast(60); // установка контраста while (!bmp280.begin(BMP280_ADDRESS - 1)) { Serial.println(F("Could not find a valid BMP280 sensor, check wiring!")); delay(100); } } void loop() { // измерение температуры, давления Tin = bmp280.readTemperature(); Press = bmp280.readPressure() / 133.3; Serial.println("Temperature: " + String(Tin) + "*C"); Serial.println("Pressure: " + String(Press) + "mm Hq"); display.clearDisplay(); //давление, мм рт.ст. { display.setTextSize(1); display.setCursor(20, 5); display.println (Press, 0); // нет знаков после запятой display.setCursor(41, 5); display.println("mmHq"); } //температура, *С { display.setTextSize(2); display.setCursor(15, 20); display.println (Tin, 1); // один знак после запятой display.setCursor(66, 20) ; display.println("С"); } display.display(); LowPower.powerDown (SLEEP_8S , ADC_OFF, BOD_OFF) ; Ресурсы, потребляемые программой барометра-термометра на Си и в Arduino IDE наглядно показаны на картинке ниже. Как видно, эти примеры потребляют 5954 байт (С) и 12956 байт (Arduino IDE) в Flash. Соотношение изменилось с 6-ти раз для «мигалки» до 2-х с небольшим. К сожалению, линейной зависимости нет - чем объемней код, тем меньше соотно- шение размеров памяти Ардуино к Си. В идеале на этой картинке должен присут- ствовать 3 столбец с кодом на Ассемблере, но такого кода в Интернете я не на- шел, а составить код самому мне пока не под силу. Попутно замечу, что использование компилируемых в Arduino IDE библиотек и функций на C/C++ особо имеет смысл в тех случаях, когда размер занимаемой па- мяти превышает или близок к размеру памяти контроллера. Мне, например, часто удается уходить от предупреждения: Недостаточно памяти, программа может рабо- тать нестабильно.
ul Г.-(I) Теперь посмотрим еще один вариант - это цифровой термометр-гигрометр на АМ2302 (DHT22), ATtinyl3 и МАХ7219, код которого составлен на Ассемблере. Автор задался целью разработать простой термометр-гигрометр выполненном на одном из самых «маленьких» микроконтроллеров — ATtiny13 с весьма скромными характеристиками - 1Кб программной памяти, 64 байтами ОЗУ и 5 интерфейсными выводами. Он решил эту непростую задачку, выбрав Ассемблер, заодно вспомнив те далекие времена, когда код можно было составлять на низкоуровневых языках, используя машины типа ZX-Spectrum. // ********************************************* // *** simple digital thermometer/hygrometer *** // ********************************************* // *** (с) SD, 14.03.2016 *** // ********************************************* II Based on ATtinyl3, AM2303 and MAX7219 // ************** // *** Clocks ***
// ************** // MCU clock frequency is 9.6MHz (internal oscillator) // Timer frequency is 75KHz = 9.6MHz/128 // (13.3 us between interrupts) #define SKIPNEXT1W (PC + 2) #define DS(var) Y + var - _dataStart / / ************ // *** pins *** // ************ // MAX7219 output pins .equ MAX_DIN = 0 .equ MAX_CS = 1 .equ MAX_CLK = 4 // AM2302 input pin .equ AM2302_PIN = 3 // MAX7219 registers .equ MAX_DECODE = 0x09 .equ MAX_INTENSITY = OxOA .equ MAX_SCANLIMIT = OxOB .equ MAX_SHUTDOWN = OxOC .equ MAX_DISPTEST = OxOF // Temperature measurement state register // Bits 0 - 2 define the byte number being received // Bit 3 is set when there are valid data received // Bits 4 - 7 define the current receiver state .def R_TS = R0 // Temperature measurement tick .def R_TT = Rl // Temperature data register .def R TD = R2 // Temperature measurement .equ TMS_NONE = .equ TMS_START = .equ TMS_ST_LOW = .equ TMS_WRSP_LOW = .equ TMS_WRSP_HIGH = signal .equ TMS_W1ST_BIT_L0W = .equ TMS_WBIT_HIGH = .equ TMS_WBIT_LOW = .equ TMS WHIGH = states 0x00 0x20 0x30 0x40 // // их // // // // // TMS_NONE - do nothing an wait until somebody changes the state L0 // Start of the measurement cycle Initial low signal is being sent (1 ms = 75 timer ticks) Initial low signal has been sent, waiting for the response low signal Response low signal has been received, // waiting for the response 0x50 // Waiting for the first bit low signal 0x60 // Waiting for the bit high signal 0x70 // Waiting for the bit low signal 0x80 // Waiting for the final high signal high // Timer 100Hz tick counter // (counts upwards from 0 to 255) .def R TICK100 = R3
// Timer 16bit 75KHz tick counter // (counts downwords from 749 to 0) .def R_TICKL = R4 .def R_TICKH = R5 // ************ // *** Data *** / / ************ .dseg dataStart: // Data start label tempData: displayData: .byte 5 .byte 4 // Data, received from the AM2302 sensor // Decimal printing result .equ DATA BUF SIZE = 8 // AM2302 data buffer size in samples // (each sample is 4 bytes) dataBuffer: .byte DATA BUF SIZE*4 .cseg .org 0 // *** Interrupts *** // Reset Handler rjmp start // IRQO Handler reti // PCINTO Handler reti // Timer0 Overflow Handler rjmp timerOvfl // EEPROM Ready Handler reti // Analog Comparator Handler reti // Timer0 CompareA Handler rjmp timerCompA // Timer0 CompareB Handler reti // Watchdog Interrupt Handler reti // ADC Conversion Handler reti // Table to convert decimal digit into 7-segment code hexTable: .db ObOllllllO, ObOOllOOOO, ObOllOllOl, ObOllllOOl
.db ObOOllOOll, ObOlOllOll, ObOlOlllll, ObOlllOOlO .db ObOlllllll, ObOllllOll start: cli ldi out R16, RAMEND (SPL), R16 // Init watchdog (4s interval) wdr ldi out ldi out R16, (1 « WDCE) (WDTCR), R16 R16, (1 « WDE) | (WDTCR), R16 | (1 « WDE) (1 « WDP3) // Init registers ldi YL, low (_dataStart) ldi YH, high (_dataStart) clr R_TS clr R_TT clr R_TICKL clr R_TICKH clr R TICK100 initl: // Init ports out (PORTB), R_TS ldi R16, (1 « MAX_DIN) out (DDRB), R16 // Init LED driver // Set all digits to "-" ldi XL, ObOOOOOOOl ldi XH, 1 rcall maxWriteWord cpi XH, 9 brne initl | (1 « MAX CS) | (1 « MAX CLK) // Set control registers ldi XL, 0 rcall maxWriteWord ldi XL, 4 rcall maxWriteWord ldi XL, 7 rcall maxWriteWord ldi XL, 1 rcall maxWriteWord ldi XH, OxOF ldi XL, 0 rcall maxWriteWord // Decode // Intensity // Scan limit // Shutdown // Display test // Init timer for 1 interrupt each 128 CPU cycles ldi R16, 127 out (OCROA), R16 ldi R16, ObOOOOOllO out (TIMSKO), R16 ldi R16, ObOOOOOOOl out (TCCROB), R16
// First part of the initialization is done. // Enable interrupts sei // Wait 2 sec (while AM2302 initialize itself) // with little animation ldi XH, 1 ldi XL, 0 init2: ldi R16, 25 rcall waitlOOHz rcall maxWriteWord cpi XH, 9 brne init2 // R6 will contain the number of // measurement values received clr R6 // R7 will contain the number of // continious errors clr R7 loop: // Reset watchdog timer wdr // Initiate measurement ldi R16, TMS_START mov R_TS, R16 loopl: // Wait for the TMS_NONE state // which indicates that the measurement // is done sleep mov R16, R_TS andi R16, OxFO brne loopl // Do we have the valid data? sbrs R_TS, 3 loop_errorl: rjmp loop_error // Check control sum of the received data ldd ldd add ldd add ldd add ldd cp brne loop R16, DS (tempData) ZL, DS (tempData + R16, ZL ZL, DS (tempData + R16, ZL ZL, DS (tempData + R16, ZL ZL, DS (tempData + R16, ZL errorl 1) 2) 3) 4) // We have valid new measurement data,
// reset error count clr R7 bufl: // Move up data in the buffer // and count the sum at the same time. // R12:R13 will contain the humidity value and // R14:R15 the temperature value clr clr clr clr ldi ldi ldd ldd std std add adc ldd ldd std std add adc R12 R13 R14 R15 ZL, low (d ZH, 0 R16, Z + 0 R17, Z + 1 Z + 4, R16 Z + 5, R17 R12, R16 R13, R17 R16, Z + 2 R17, Z + 3 Z + 6, R16 Z + 7, R17 R14, R16 R15, R17 (dataBuffer + (DATA BUF SIZE - 2)*4) subi ZL, 4 cpi ZL, low (dataBuffer - 4) brne bufl // Add new humidity value to the buffer // and to the sum ldd R16, DS (tempData + 1) ldd R17, DS (tempData) std DS (dataBuffer + 0), R16 std DS (dataBuffer + 1), R17 add R12, R16 adc R13, R17 // Add new temperature value to the buffer // and to the sum ldd R16, DS (tempData + 3) ldd R17, DS (tempData + 2) // Check for a negative value and R17, R17 brpl buf2 // Convert negative temperature to the 2fs // complement form clr andi neg sbc mov R17, ZL 0x7F R16 ZL, R17 R17, ZL buf2:
buf3: std DS (dataBuffer + 2), R16 std DS (dataBuffer + 3), R17 add R14, R16 adc R15, R17 // Divide the humidity and temperature // sum values by 8 (by shifting them right // three ldi asr ror asr ror dec brne times) R16, R15 R14 R13 R12 R16 buf3 //Do we have 8 full measurements? mov Rl6, R6 cpi R16, 7 // If so, use the average values from // the buffer breq buf4 // Otherwise use the latest measurement ldd R12, DS (dataBuffer + 0) ldd R13, DS (dataBuffer + 1) ldd R14, DS (dataBuffer + 2) ldd R15, DS (dataBuffer + 3) inc R6 buf4: // Print out values // *** Humidity *** movw X, R12 rcall printDecX ldi XH, 1 ldd XL, DS (displayData + 3) rcall maxWriteWord ldd XL, DS (displayData + 2) ori XL, 0x80 rcall maxWriteWord ldd XL, DS (displayData + 1) rcall maxWriteWord ldd XL, DS (displayData) rcall maxWriteWord // *** Temperature *** movw X, R14 // Check for a negative value and XH, XH brpl buf5
// Calculate the absolute value clr ZL neg XL sbc ZL, XH mov XH, ZL buf5: rcall printDecX ldi XH, 5 ldd XL, DS (displayData + 3) rcall maxWriteWord ldd XL, DS (displayData + 2) ori XL, 0x80 rcall maxWriteWord ldd XL, DS (displayData + 1) rcall maxWriteWord // If temperature is negative // write the minus sign to the first digit // (temperatures of -100.0 and below // are not supported anyway) ldd XL, DS (displayData) and R15, R15 brpl SKIPNEXT1W ldi XL, 1 rcall maxWriteWord loop2: // Wait for 1 sec ldi R16, 100 rcall waitlOOHz // And repeat rjmp loop loop_error: // An error had occured. // Increment error count inc R7 //Do we have 3 or more errors in a row? mov Rl б, R7 cpi R16, 3 // No? Just do nothing brne loop2 // Prevent error count from growing dec R7 // Display error ldi ZL, low (errText*2) ldi ZH, high (errText*2) rcall maxWrite8Bytes rjmp loop2
errText: // "Sn Error" .db ObOOOOOlOl, ObOOOlllOl, ObOOOOOlOl, ObOOOOOlOl .db ObOlOOllll, ObOOOOOOOO, ObOOOlOlOl, ObOlOllOll // ********** // Waits given number (R16) of 100Hz ticks // Uses: Z waitlOOHz: // Enable sleep ldi ZL, ObOOlOOOOO out (MCUCR) , ZL mov ZL, R TICK100 wlOO: sleep mov sub cp brcs ret wlOO ZH, ZH, ZH, R_TICK100 ZL R16 // Timer interrupt timerOvf1: timerCompA: push Rl 6 in push Rl 6 push ZL push ZH R16, (SREG) // Receive AM2303 data rcall am2302proc // Decrement current 75KHz tick ldi R16, 1 sub R_TICKL, R16 brcc timerRet sub R_TICKH, R16 brcc timerRet // Initialize 75KHz tick value ldi ZL, low (750 - 1) ldi ZH, high (750 - 1) movw R_T ICKL, Z // Increment current 100Hz tick inc R TICK100 timerRet: pop pop pop out pop reti ZH ZL R16 (SREG), R16 R16
// ************** // *** АМ2302 *** / / ************** amStart: // Send the start low signal. // Switch corresponding PORTB pin to output // (there is already 0 in the PORTB register) sbi (DDRB) , AM2302_PIN ldi R16, TMS_ST_LOW rjmp amSetState amStartLow: // Initial start low signal is being sent. // Wait for 75 ticks cpi R16, 75 brne amNone // Switch PORTB pin back to input cbi (DDRB), AM2302_PIN ldi R16, TMS_WRSP_LOW // Do not check AM2303 input pin at this tick // since it's possible that it has not recovered // from the low state yet. rjmp amSetState amWRespLow: // Waiting for the response low signal sbrc ZH, AM2302_PIN ret ldi R16, TMS_WRSP_HIGH rjmp amSetState amWRespHigh: // Waiting for the response high signal sbrs ZH, AM2302_PIN ret ldi R16, TMS_W1ST_BIT_L0W rjmp amSetState amWl S tBi tLow: // Waiting for the first bit low signal sbrc ZH, AM2302_PIN ret // Get ready to receive the first bit ldi R16, 1 mov R_TD , Rl 6 // Set new state and reset the byte counter ldi ZL, TMS_WBIT_HIGH rjmp amSetState2 amBitHigh: sbrs ZH, AM2302_PIN ret
// If the bit low signal was there too long // (longer than 5 ticks (5*13.3 = 66.5us) // something went wrong) cpi R16, 6 brcc amResetState ldi R16, TMS_WBIT_LOW rjmp amSetState am2302proc: // First, check for the TMS_NONE state. // In this case just do nothing to // not waste MCU cycles, mov ZL, R_TS andi ZL, OxFO cpi ZL, TMS_NONE breq amNone // Increment receiver tick inc R_TT // If we are waiting for too long, // something went wrong, reset the state breq amResetState // Save the current tick into a more // convenient register mov Rl6, R_TT // Get input signal in ZH, (PINB) // Branch depending on the current state. // Check for TMS_WBIT_LOW first since it // has the longest service routine cpi ZL, TMS_WBIT_LOW breq amBitLow cpi ZL, TMS_START breq amStart cpi ZL, TMS_ST_LOW breq amStartLow cpi ZL, TMS_WRSP_LOW breq amWRespLow cpi ZL, TMS_WRSP_HIGH breq amWRespHigh cpi ZL, TMS_W1ST_BIT_L0W breq amWlStBitLow cpi ZL, TMS_WBIT_HIGH breq amBitHigh cpi ZL, TMS WHIGH
breq amWHigh amResetState: // In case of an error, reset state to // the default TMS_NONE ldi R16, TMS_NONE amSetState: // Preserve the current byte number mov ZL, R_TS andi ZL, 0x07 or ZL, R16 amSetState2: mov R_TS, ZL // Clear receiver tick counter clr R_TT amNone: ret amBitLow: sbrc ZH, AM2302_PIN ret // The high bit signal was too long? cpi R16, 8 brcc amResetState // Store input bit (inverted, since cpi produces // inverted result in the carry flag) cpi R16, 4 rol R_TD // Initally we set R_TD to 1, so when all 8 // bits are received, the carry flag will be set // indicating that a full byte has been received. // Otherwise, receive the next bit ldi R16, TMS_WBIT_HIGH brcc amSetState // We have the full byte. Invert it com R_TD // Save it mov ZL, R_TS andi ZL, 0x07 subi ZL, low (-tempData) ldi ZH, high (tempData) st Z+, R_TD // Did we receive all 5 bytes? cpi ZL, low (tempData + 5) ldi R16, TMS_WHIGH breq amSetState // OK, receive the next byte. // Increment the byte counter
inc R TS // Initialize R_TD ldi R16, 1 mov R_TD , Rl 6 ldi R16, TMS_WBIT_HIGH rjmp amSetState amWHigh: sbrs ZH, AM2302_PIN ret cpi R16, 6 brcc amResetState // We received everything. Set // the state to TMS_NONE and set // the data validity bit ldi R16, 0x08 mov R_TS, R16 ret // ********* /* // Write data from Z // Uses R16 - R19, X, Z maxWriteData: lpm XH, Z+ tst XH brne SKIPNEXT1W ret lpm XL, Z+ rcall maxWriteWord rjmp maxWriteData maxlnit: .db .db .db .db .db .db MAX_DECODE, 0 MAX_INTENSITY, 4 MAX_SCANLIMIT, 7 MAX_SHUTDOWN, 1 MAX_DISPTEST, 0 0, 0 maxTest: .db 0, ObOOOlllOl, ObOOOlOlOl, ObOOOlOOOO, ObOOOlllOO, ObOOllllOl, ObOOOOOlOl, ObOlllOlll */ // Writes 8 bytes from (Z) (program memory) // to MAX7219 // Uses R16 - R19, X, Z maxWrite8Bytes: ldi XH, 0x01 mw8bl: lpm XL, Z+ rcall maxWriteWord
cpi XH, 9 brne mw8bl ret // Write word X (XL = data, XH = address) to MAX2719 // Uses R16 - R19, X maxWri teWord: // Set all pins to zero in R17, (PORTB) andi R17, -((1 « MAX_DIN) | (1 « MAX_CS) | (1 « MAX_CLK) ) out (PORTB), R17 ldi R19, (1 « MAX_CLK) mov R16, XH rcall mwwl mov Rl6, XL rcall mwwl // Set LOAD(CS) to high thus writing all 16 bits into // MAX register sbi (PORTB), MAX_CS // Increment MAX register number inc XH ret mwwl: mww2: ldi R18, 8 bst bid out lsl dec R16, 7 R17, MAX_DIN (PORTB), R17 R16 R18 // Create clock impulse by toggling clock output twice out (PINB), R19 out (PINB), R19 brne mww2 ret printDecX: ldi ZH, low (1000) ldi R16, high (1000) rcall pdx // Change zero digit to empty space cpi ZL, ObOllllllO brne SKIPNEXT1W ldi ZL, 0 std DS (displayData), ZL ldi ZH, 100
ldi R16, О rcall pdx // If this digit is zero and the first // digit is empty (i.e. it was zero too) // change this digit to empty space ldi R16, ObOllllllO eor R16, ZL ldd ZH, DS (displayData) or R16, ZH brne SKIPNEXT1W ldi ZL, 0 std DS (displayData + 1), ZL ldi ZH, 10 ldi R16, 0 rcall pdx std DS (displayData + 2), ZL mov ZL, XL rcall pdx3 std DS (displayData + 3), ZL // Clear carry flag to indicate that // no error occurred clc ret ldi i . ±. sub sbc brcs cpi breq inc rjmp 2: add adc ZL, 0 XL, ZH XH, R16 pdx2 ZL, 9 pdxOverflow ZL pdxl XL, ZH XH, R16 pdx3: subi ZL, -low (hexTable « 1) ldi ZH, high (hexTable « 1) lpm ZL, Z ret pdxOverflow: // Set carry flag to indicate error sec // Pop return address out of the stack // so we can return to the caller of printDecX pop Rl6 pop Rl6 ret
Ниже скриншот со сборкой данного кода в Atmel Studio 7. Код устройства на Ассемблере занимает 738 байт памяти в контроллере. Безус- ловно, программа барометра-термометра, о котором шла речь выше, будь ее код составлен на Ассемблере, заняла бы больше места. По нескольким причинам — в схеме реализовано управление дисплеем Nokia3110 по интерфейсу SPI (это 5 ли- ний связи, тут - 3) , связь с датчиком ВМР280 осуществляется по протоколу 12С (2 линии, тут - 1) и дополнительные символы, которые позволяют не гадать - температура это или другой параметр. Из того, что я нашел в Интернете, можно утверждать, Ассемблер даст выигрыш в размере кода для относительно больших проектов процентов 10-20 по сравнению с Си. Но надо учитывать, что в больших проектах Си может уменьшить размер ко- да за счёт лучшей оптимизации. Код Ассемблера выполняется практически на машинном уровне: один цикл - одна команда. В качестве аргумента приведу пример из справочника по командам ас- семблера AVR. Установка бита в регистре ввода/вывода — SBI А, Ь. Эта команда устанавливает заданный бит в регистре ввода-вывода. На выполнение этой опера- ции контроллерами megaAVR потребуется 2 цикла и на tinyAVR, XMEGA — 1 цикл. Для схемы с контроллером AT tiny 13 и резонатором 9,6 МГц выполнение команды займет один цикл, то есть 1/9600000 Гц = 0,104 мксек. Выполнение похожей операции на языке Си, например, задать состояние порта — PORTB = 32; займет в этой же схеме не меньше времени. А о Ардуино и говорить нечего - там придется выполнить объемную функцию void digitalWrite(uint8_t pin, uint8_t val);. Поэтому разработчики простых в управлении серийных продуктов (холодильник, кофеварка без наворотов, другое — оглянитесь вокруг себя дома), как правило, пишут коды на низкоуровневых языках. С тем, чтобы разместить программу в кон- троллере с меньшей памятью. Тут работают законы экономики — контроллер с меньшими ресурсами стоит дешевле, следовательно себестоимость изделия стано- вится ниже.
Теперь о энергосбережении немножко издали. Вспомним, что код Ассемблера вы- полняется на машинном уровне: один цикл - одна или несколько команд, в зави- симости от типа контроллера. Это — десятые доли микросекунды. То есть, на вы- полнение программы с размером несколько десятков байт уйдут единицы-десятки микросекунд. Дальше контроллер бесконечно будет крутить этот набор «О» и «1», затрачивая энергию на перезаряд емкости затворов сотен полевых транзисторов, на которых построен кристалл контроллера, а также чтение и записи данных в его память. Длительность периода повтора будет зависеть только от размера ко- да в памяти контроллера, неважно на каком языке он составлен. Просто на Assembler'e он будет наименьшим, а в Arduino IDE - наибольшим. Соответствен- но, период цикла для кода на Assembler'e - наименьший, в Arduino IDE - наи- больший . Уменьшить эти затраты можно остановив процессор или программно уменьшив частоту его работы. В Ассемблере переход в "спящий" режим сна выполняет функ- ция управления контроллером SLEEP. В других можно использовать функцию WDT (WatchDog Timer), а в Ардуино еще и функцию LowPower. powerDown (SLEEP_1S, ADC_OFF, BOD_OFF), заодно отключив все лишнее, что не используется в конкрет- ной задаче. В эффективности этой функции сможет убедиться каждый, заменив в скетче «мигалки» функцию отсчета времени delay(1000); этой функцией и включив в разрыв питания контроллера амперметр. Да, не забудьте подключить библиотеку LowPower.h. Ток в цепи питания attinyl3a с паузой — 1,5мА, со сном — 240мкА. Потребление в 6(!) раз меньше. Допустим, вы намерены собрать барометр-термометр и задумываетесь о энерго- сбережении. Понятно, что давление/температура в заданной разрядности не изме- нятся за несколько минут, которые для контроллера целая вечность. Ему можно выделить это время для сна. После сна он снова выполнит свою работу: примет информацию с датчика, преобразует в понятные для человека циферки и выведет все это на дисплей. И в таком режиме «работа-сон» он будет крутиться, пока не сядут батарейки. Объем «работы» контроллера, вернее время, которое контроллер будет занят выполнением работы, зависит от того, на каком языке составлена программа барометра-термометра. Если есть возможность загрузить в контроллер код на выбор - ArduinoIDE, С, Assembler, с одинаковым временем «сна», то в каком из трех предложенных вариантов батарейки сядут раньше (позже)? Мой от- вет - ArduinoIDE (Assembler). Так куда же идти? На мой взгляд, для любителей, как я, - это платформа Arduino IDE с низкоуровневыми вставками. Тем же, кому тесно в Arduino IDE, — в С. Хотя коды на С можно оптимизировать иногда до размеров не намного боль- ше, чем в Assembler'е, все-таки для понимания работы контроллера стоит на- прячься и освоить азы Assembler'а. Ведь полезность знаний - это аксиома. Arduino на ассемблере Попробуем на простом примере рассмотреть, как можно ЛЛхакнуть" Arduino Uno и начать писать программы в машинных кодах, т.е. на ассемблере для микрокон- троллера ATmega328p. На данном микроконтроллере собственно и собрана большая часть недорогих «классических» плат «duino». Данный код также будет работать на практически любой demo плате на ATmega328p и после небольших возможных до- работок на любой плате Arduino на Atmel AVR микроконтроллере. В примере я по- старался подойти так близко к железу, как это только возможно. Для лучшего понимания того, как работает микроконтроллер не будем использовать какие-либо готовые библиотеки, а уж тем более Arduino IDE. Arduino очень клевая штука, но многое из того что происходит с микрокон- троллером специально спрятано в дебрях библиотек и среды Arduino для того
чтобы не пугать новичков. Поигравшись с мигающим светодиодом я захотел по- нять , как микроконтроллер собственно работает. Помимо утоления чисто познава- тельного зуда, знание того как работает микроконтроллер и стандартные средст- ва общения микроконтроллера с внешним миром — это называется «периферия», да- ет преимущество при написании кода как для Arduino так и при написания кода на С/Assembler для микроконтроллеров а также помогает создавать более эффек- тивные программы. Микроконтроллер ATmega328P является 8 разрядным микроконтроллером, предна- значенным для встраиваемых приложений. Он изготавливается по малопотребляющей КМОП технологии, которая в сочетании с усовершенствованной RISC архитектурой позволяет достичь наилучшего соотношения быстродействие/энергопотребление. Микроконтроллер построен по двухшинной (гарвардской) архитектуре и имеет раз- дельные шины памяти программ и памяти данных. /- Шина команд 16 бит ^ II ч,> Процессор с 3JL Паыять команд Памятьданчык 1 Устройства ввода'&ывода л Р Шина данных 8 бит Подсистема ввода-вывода: ■ 3 порта ввода-вывода (23 линии): В (8 линий), С (7 линий) и D (8 линий); ■ программное конфигурирование и выбор портов ввода/вывода; ■ выводы могут быть запрограммированы как входные или как выходные независи- мо друг от друга; ■ входные буферы с триггером Шмитта на всех выводах; ■ возможность подключения ко всем входам внутренних подтягивающих резисторов (сопротивление резисторов составляет 35...120 кОм). Периферийные устройства: ■ 8 разрядные таймеры/счетчики (таймеры ТО и Т2); ■ 16 разрядный таймер/счетчик (таймер Т1); ■ сторожевой таймер WDT; ■ 6 каналов ШИМ (широтно-импульсная модуляция); ■ аналоговый компаратор; ■ 6-ти канальный 10 разрядный АЦП; ■ полнодуплексный универсальный синхронный/асинхронный приемопередатчик; ■ последовательный синхронный интерфейс SPI; ■ последовательный двухпроводный интерфейс TWI (аналог интерфейса I2C). Арифметико-логическое устройство (АЛУ), выполняющее все вычисления, подключено непосредственно к 32 рабочим регистрам, объединенным в регистровый файл. АЛУ выполняет одну операцию (чтение регистров, выполнение операции и запись результата в регистр) за один машинный цикл. Практически каждая из команд (за исключением команд, у которых одним из операндов является 16 разрядный адрес) занимает одну ячейку памяти программ.
+ il Instruction Register регистр команд SPI Последовательный периферийный интерфейс Program Counter счетчик команд РОН 32 регистра общего назначения \< Н I/O Ports Порты ввода/вывода Interrupts Прерывания Timer/Counters Таймеры/счетчики АС Аналоговый компаратор A/D Converter Аналого-цифровой I преобразователь ' WDT Сторожевой таймер Интерфейс JTAG Конвейеризация заключается в том, что во время исполнения текущей команды производится выборка из памяти и дешифрация кода следующей команды. В соответствии с Гарвардской архитектурой разделены не только адресные про- странства памяти программ и памяти данных, но также и шины доступа к ним. Способы адресации и доступа к этим областям памяти также различны. Такая структура позволяет центральному процессору работать одновременно как с памя- тью программ, так и с памятью данных, что существенно увеличивает производи- тельность . Каждая из областей памяти данных (ОЗУ и EEPROM) также расположена в своем адресном пространстве. Память программ предназначена для хранения команд, управляющих функциониро- ванием микроконтроллера. Память программ представляет собой электрически сти- раемое ППЗУ (FLASH ПЗУ). Память программ имеет 16 разрядную организацию, поэтому для ATmega328P ее длина равна 16 К (16x1024) 16-ти разрядных слов. Логически память программ разделена на две неравные части — область при- кладной программы и область загрузчика (2 КБ). В последней может располагать- ся специальная программа (загрузчик), позволяющая микроконтроллеру самостоя- тельно управлять загрузкой и выгрузкой прикладных программ. Для адресации памяти программ используется 16-ти разрядный счетчик команд (Program Counter). По адресу $0000 памяти программ находится вектор сброса. После инициализа- ции (сброса) микроконтроллера выполнение программы начинается с этого адреса (по этому адресу должна размещаться команда перехода к инициализационной час- ти программы). Начиная с адреса $0002 располагается таблица векторов прерыва- ний.
Application Flash Section 0x0000 Boot Flash Section 0x3FFF Память данных микроконтроллеров семейства Меда разделена на три части: ■ регистровая память, ■ оперативная память (статическое ОЗУ) ■ энергонезависимое ЭСППЗУ (EEPROM). IN/OUT 0x0000-0x001 F 32 registers 64 I/O registers 160 Ext I/O registers Internal SRAM (2048x8) Load/Store 0x0000-0x001 F 0x0020 - 0x005F 0x0060 - OxOOFF 0x0100 0x08FF Регистровая память включает: ■ 32 регистра общего назначения (РОН), объединенных в файл, ■ Служебные регистры ввода/вывода (РВВ) и дополниельные регистры ввода- вывода (ДРВВ). Под РВВ в памяти микроконтроллера отводится 64 байта, а под ДРВВ - 160 байт. Оперативная память (статическое ОЗУ) объемом 2 Кбайт служит для хранения переменных программ помимо регистров общего назначения. Энергонезависимая постоянная память служит для долговременного хранения различной информации, которая может изменяться в процессе функционирования готовой системы (калибровочные константы, серийные номера, ключи и т. п.). Ее
объем 1 Кбайт. Эта память расположена в отдельном адресном пространстве, а доступ к ней осуществляется с помощью специальных регистров ввода-вывода (РВВ) . Используется линейная организация памяти. Первые 256 ячеек отведены под ре- гистры: 32 штуки регистры общего назначения РОН, остальное - регистры ввода- вывода (РВВ и ДРВВ). Все регистры общего назначения объединены в регистровый файл быстрого дос- тупа. Все 32 РОН непосредственно доступны AJIY Любой РОН может использоваться практически во всех командах и как операнд источник и как операнд приемник. Это позволяет АЛУ выполнять одну операцию (извлечение операндов из регистро- вого файла, выполнение команды и запись результата обратно в регистровый файл) за один машинный цикл. Каждый регистр файла имеет свой собственный адрес в пространстве памяти данных. Поэтому к ним можно обращаться двумя способами (как к регистрам и как к памяти). 7 о Адрес Рабочие регистры общего назначения R0 R1 R2 R13 R14 R15 R16 R17 R26 R27 R28 R29 R30 R31 0x00 0x01 0x02 OxOD ОхОЕ OxOF 0x10 0x11 0ж1А 0x1 В 0х1С 0x1 D 0х1Е 0x1F Х-регистр Младший байт Х-регистр Старший боит Y-регистр Младший байт Y-регистр Старший байт Z-регистр Младший байт Z-регистр Старший байт Последние 6 регистров файла (R26...R31) могут также объединяться в три 16 разрядных регистра X, Y и Z, используемых в качестве указателей при косвенной адресации памяти данных. 15 X - register |7 R27($1B) 15 Y - register | 7 R29($1D) 15 Z-r agister Г R31 ($1F) XH YH ZH 0 7 R26($1A) 0 7 R28($1C) R30($1E) XL YL ZL
Все регистры ввода/вывода (РВВ) условно можно разделить на две группы: ■ служебные регистры микроконтроллера ■ регистры, относящиеся к конкретным периферийным устройствам. Регистры ввода/вывода располагаются в так называемом основном пространстве ввода/вывода, в котором их адреса начинаются с 0x00 до 0x3F. При этом по ука- занным адресам к ним можно обратиться с помощью команд IN и OUT. Однако эти регистры также представлены в общем адресном пространстве стати- ческой памяти как ячейки ОЗУ, где их адреса начинаются с 0x20 до 0x5F (т.е. сдвинуты на константу 0x20). Как к ячейкам ОЗУ к регистрам можно обратиться с помощью команд ST/SD/SDD и LD/LDS/LDD. К дополнительным регистрам ввода-вывода (ДРВВ) можно обращаться только как к ячейкам памяти. Регистровый Адресное файл пространств о то 1 R' 1 к 1 R3' S30CO S0001 SG0CC SOO'F Регистр* мода вывода S0O I so' I S3f 50020 Г S0021 1 S305.* 1 Дог. регистры ввода вывода S60 S6' SFF S0O60 sooei S00FF Регистр состояния SREG располагается по адресу $3F ($5F) и содержит набор флагов, показывающих текущее состояние микроконтроллера. Большинство флагов автоматически устанавливаются в «1» или сбрасываются в «О» при наступлении определенных событий: Bit 0x3F (0x5F) Read/Write Initial Value с 7 1 R/W 0 6 T R/W 0 5 H R/W 0 4 S R/W 0 3 V R/W 0 2 N R/W 0 1 Z R/W 0 0 С R/W 0 3 SREG
Раз- ряд 7 6 5 4 3 2 1 0 Наз- вание I Т Н S V N Z С Описание Общее разрешение прерываний. Для разрешения прерываний этот флаг должен быть установлен в «1». Разрешение/запрещение отдельных прерываний производится установкой или сбросом соответствующих разрядов регистров масок прерываний (регистра управления прерыва- ниями) . Если флаг сброшен, то прерывания запрещены независимо от состояния разрядов этих регистров. Флаг сбрасывается аппаратно после входа в прерывание и восстанавливается командой RETI для разрешения обработки следующих прерываний Хранение копируемого бита. Этот разряд регистра используется в качестве источника или приемника команд копирования битов BLD (Bit LoaD) и BST (Bit STore) . Заданный разряд любого РОН может быть скопирован в этот разряд командой BST или установлен в соот- ветствии с содержимым данного разряда командой BLD Флаг половинного переноса. Этот флаг устанавливается в «1», если произошел перенос из младшей половины байта (из 3-го разряда в 4- й) или заем из старшей половины байта при выполнении некоторых арифметических операций Флаг знака. Этот флаг равен результату операции «Исключающее ИЛИ» (XOR) между флагами N (отрицательный результат) и V (переполнение числа в дополнительном коде). Соответственно этот флаг устанавли- вается в «1», если результат выполнения арифметической операции меньше нуля Флаг переполнения дополнительного кода. Этот флаг устанавливается в «1» при переполнении разрядной сетки знакового результата. Ис- пользуется при работе со знаковыми числами (представленными в до- полнительном коде). Более подробно — см. описание системы команд Флаг отрицательного значения. Этот флаг устанавливается в «1», если старший (7-й) разряд результата операции равен «1». В про- тивном случае флаг равен «0» Флаг нуля. Этот флаг устанавливается в «1», если результат выпол- нения операции равен нулю Флаг переноса. Этот флаг устанавливается в «1», если в результате выполнения операции произошел выход за границы байта Каждый порт ввода-вывода микроконтроллеров состоит из определенного числа выводов, через которые микроконтроллер может осуществлять прием и передачу цифровых сигналов. Задание направления передачи данных через любой контакт ввода/вывода может быть произведено программно в любой момент времени. Микроконтроллер ATmega8x имеют три порта ввода/вывода: ■ порт В (8 разрядный), ■ порт С (7 разрядный), ■ порт D (8 разрядный). Обращение к портам производится через регистры ввода/вывода. Под каждый порт в адресном пространстве ввода/вывода зарезервировано по 3 адреса, по ко- торым размещены следующие регистры: ■ регистр данных порта PORTx, ■ регистр направления данных DDRx, ■ регистр выводов порта PINx. Регистр PINx доступен только для чтения, a PORTx и DDRx доступны как для чтения, так и для записи. В таблице приведены адреса регистров микроконтроллера ATmega328P.
Порт В С D Регистр PORTB DDRB PINB PORTC DDRC PINC PORTD DDRD PIND Адреса $18 ($38) $17 ($37) $16 ($36) $15 ($38) $14 ($37) $13 ($36) $12 ($32) $11 ($31) $10 ($30) &С1 [ RESET ]—[ РС6 ] поч RXD PD0 GZH TXD { PD1 )—т\ СЕН СЕН INTO ]—[ PD2 } XTAL1 БС2 }—[ XTAL2 H Tl СЕН AINO СЕН AIN1 EM CLKO INT1 }- -{ PD3 \ XCK j- -[ PD4 } 1 1 PB6 } ] ( PB7 } ) ( PD5 } PD6 PD7 )—[ pbo )—•! •—[ •—[ —[ •—[ •-{ •—( PC5 ' PC4} PC3 j PC2 j PCI j PCO^ PB5 И SCK H PB4 И MISO H рвз и OC2A PB2 H OC1B •—{ PB1 OC1A HED Конфигурирование портов ввода-вывода: Разряд DDxn регистра DDx определяет направление передачи данных через кон- такт ввода/вывода. Если этот разряд установлен в «1», то n-й вывод порта является выходом, если же сброшен в «0» — входом. Разряд PORTxn регистра PORTx выполняет двойную функцию. 1. Если вывод функционирует как выход (DDxn = «1») , этот разряд определяет со стояние вывода порта. Если разряд установлен в «1», на выводе уста- навливается напряжение ВЫСОКОГО уровня. Если разряд сброшен в «О», на выводе устанавливается напряжение НИЗКОГО уровня. 2. Если вывод функционирует как вход (DDxn = «0») , разряд PORTxn определяет состояние внутреннего подтягивающего резистора для данного вывода. При установке разряда PORTxn в «1» подтягивающий резистор подключается между выводом микроконтроллера и проводом питания. DDRxn 0 0 1 1 PORTxn 0 1 0 1 Состояние линии I (Input) Вход I (Input) Вход 0 (Output) Выход 0 (Output) Выход Описание Высокоимпендансный вход. (Не рекомендую ис- пользовать , так как могут наводится наводки от питания) Подтянуто внутренне сопротивление На выходе низкий уровень На выходе высокий уровень
В качестве учебно-тренировочной задачи попробуем сделать самое простое, что только возможно — правильно и полезно подергать одной ногой микроконтроллера, ну то есть будем читать данные из датчика температуры и влажности DHT-11. Итак, будем делать все наиболее близко к железу, у нас есть: плата совмес- тимая с Arduino Uno, датчик DHT-11, три провода, Atmel Studio и машинные ко- ды. Для начала подготовим нужное оборудование. Писать код будем в Atmel Studio 7 — бесплатно скачивается с сайта произво- дителя микроконтроллера — Atmel1,2. Web-установщик есть в архиве: ftp://homelab.homelinuxserver.org/pub/arhiv/2021-09-a3.rar Весь код запускался на клоне Arduino Uno — у меня это DFRduino Uno от DFRobot, на контроллере ATmega328p работающем на частоте 16 MHz — отличная надежная плата. Каких-либо отличий от стандартного Uno в процессе эксплуата- ции я не заметил. Похожая чорная плата от DFBobot, только ЛЛМеда" отлетала у меня 2 года в качестве управляющего контроллера квадрокоптера — куда ее толь- ко не заносило — проблем не было. Для просмотра сигналов длительностью в микросекунды (а это на минутку 1 миллионная доля секунды) , я использовал штуку, которая называется ЛЛлогический анализатор". Конкретно, я использовал клон восьмиканального USBEE AX Pro. Как смотреть для отладки такие быстрые процессы без осциллографа или логического анализатора — на самом деле даже не знаю, ничего посоветовать не могу. Прежде всего я подключил свой клон Uno — как я говорил у меня это DFRduino Uno к Atmel Studio 7 и решил попробовать помигать светодиодиком на ассембле- ре. Код пишется прямо в студии, прошивать плату можно через USB порт исполь- зуя привычные возможности загрузчика Arduino - через AVRDude. Можно шить и через внешний программатор, я пробовал на китайском USBASP, по факту у меня оба способа работали. В обоих случаях надо только правильно настроить проши- вальщик AVRDude, пример моих настроек на картинке: External Tools ? ШШ Menu contents: Delete T,tle: DFRdumccnUSB Command: C: AVRDUDE'a.rdude.exe Arguments: -С С avrdude a.rdude.ccnf -p atmega^Sp -< ► Initial directory: > ^ Use Output window Prompt for arguments Treat output as Unicode ^ OK Cancel 1 http://studio.download.atmel.com/7.0.2397/as-installer-7.0.2397-web.exe 2 http://atmel-studio.s3-website-us-west-2.amazonaws.com/7.0.2397/as-installer- 7.0.2397-full.exe
Полная строка аргументов: -С ЛЛС: \avrdude\avrdude. conf" -р atmega328p -с arduino -Р С0М7 115200 -U flash:w:"$(ProjectDir)Debug\$(TargetName).hex:i В итоге, для простоты я остановился на прошивке через USB порт — это стан- дартный способ для Arduio. На моей UNO стоит чип ATmega 328Р, его и надо ука- зать при создании проекта. Нужно также выбрать порт к которому подключаем Arduino — на моем компьютере это был СОМ7. Для того, чтобы просто помигать светодиодом никаких дополнительных подклю- чений не нужно, будем использовать светодиод, размещенный на плате и подклю- ченный к порту Arduino D13 — напомню, что это 5-ая ножка порта «PORTB» кон- троллера . Подключаем плату через USB кабель к компьютеру, пишем код в студии, проши- ваем прямо из студии. Основная проблема здесь собственно увидеть это мигание, поскольку контроллер фигачит на частоте 16 MHz и, если включать и выключать светодиод такой же частотой мы увидим тускло горящий светодиод и собственно все. Для того чтобы увидеть, когда он светится и когда он потушен, мы зажжем светодиод и займем процессор какой-либо бесполезной работой на примерно 1 се- кунду. Саму задержку можно рассчитать вручную зная частоту — одна команда вы- полняется за 1 такт. После установки задержки, код выполняющий примерно то же что делает классический «Blink» Arduino может выглядеть примерно так: 1 з г, с 7 я ч 1С 11 12 13 1- 15 1с 17 18 19 2£ 21 22 loop: L1: cli sbi sbi Idi Idi Idi dec Drne dec ere dec Ьте nop DDRB, PORTB ПБ, П9, Г23, г 23 LI rl9 LI ПБ LI 5 . 5 S2 43 0 in R16, PORTB Idi EOR cut rjrnp R17, R16, PORTB loop ; PORT В., Pin 5 - на бы кед ; выставили на Pin 5 гог единицу ; delay 1Э8Э ms ; перек.гкчи.ги XOR 5-ый бит в порту 3593180800 R17 , R16 Но на самом деле так писать не очень хорошо, поскольку мы полностью похери- ли такие важные штуки, как стек и вектор прерываний (о них — позже). Ок, светодиодиком помигали, теперь для того чтобы практика работа с GPIO была более или менее осмысленной прочитаем значения с датчика DHT11 и сделаем
это также целиком на ассемблере. Для того чтобы прочитать данные из датчика нужно в правильной последова- тельность выставлять на рабочей линии датчика сигналы высокого и низкого уровня — собственно это и называется дергать ногой микроконтроллера. С одной стороны, ничего сложного, с другой стороны все какая-то осмысленная деятель- ность — меряем температуру и влажность — можно сказать сделали первый шаг к построению какой ни будь «Погодной станции» в будущем. Забегая на один шаг вперед, хорошо бы понять, а что собственно с прочитан- ными данными будем делать? Ну, хорошо прочитали мы значение датчика и устано- вили значение переменной в памяти контроллера в 23 градуса по Цельсию, соот- ветственно . Как посмотреть на эти цифры? Решение есть! Полученные данные я буду смотреть на большом компьютере, выводя их через USART контроллера через виртуальный СОМ порт по USB кабелю прямо в терминальную программу типа PuTTY. Для того чтобы компьютер смог прочитать наши данные будем использовать преоб- разователь USB-TTL — такая штука, которая и организует виртуальный СОМ порт в Windows. Сама схема подключения может выглядеть примерно так: Сигнальный вывод датчика подключен к ноге 2 (PIN2) порта PORTD контролера или (что то же самое) к выводу D2 Arduino. Он же через резистор 4.7 kOm "подтянут" на ЛЛплюс" питания. Плюс и минус датчика подключены — к соответст- вующим проводам питания. USB-TTL переходник подключен к выходу Тх USART порта Arduino, что значит PIN1 порта PORTD контроллера. Разбираемся с датчиком и смотрим datasheet. Сам по себе датчик несложный, и использует всего один сигнальный провод, который надо подтянуть через рези- стор к +5V — это будет базовый «высокий» уровень на линии. Если линия свобод- на — т.е. ни контроллер, ни датчик ничего не передают, на линии как раз и бу- дет базовый «высокий» уровень. Когда датчик или контроллер что-то передают,
то они занимают линию — устанавливают на линии «низкий» уровень на какое-то время. Всего датчик передает 5 байт. Байты датчик передает по очереди, снача- ла показатели влажности, потом температуры, завершает все контрольной суммой, это выглядит как ЛЛННТТХХ", в общем, смотрим datasheet. Пять байт — это 40 бит и каждый бит при передаче кодируется специальным образом. Для упрощения, будет считать, что «высокий» уровень на линии — это «едини- ца», а «низкий» соответственно «ноль». Согласно datasheet для начала работы с датчиком надо положить контроллером сигнальную линию на землю, т.е. получить «ноль» на линии и сделать это на период не менее чем 20 мсек (миллисекунд), а потом резко отпустить линию. В ответ — датчик должен выдать на сигнальную ли- нию свою посылку, из сигналов высокого и низкого уровня разной длительности, которые кодируют нужные нам 40 бит. И, согласно datasheet, если мы удачно прочитаем эту посылку контроллером, то мы сразу поймем что: а) датчик собственно ответил, б) передал данные по влажности и температуре, с) передал контрольную сумму. В конце передачи датчик отпускает линию. Ну и в datasheet написано, что датчик можно опрашивать не чаще чем раз в секунду. Итак, что должен сделать микроконтроллер, согласно datasheet, чтобы датчик ему ответил — нужно прижать линию на 20 миллисекунд, отпустить и быстро смот- реть , что на линии: VDD у GND 4 >lto" * / / Release ihe bus l / after the host down г Host signal Slave signal Host sends a slart signal Датчик должен ответить — положить линию в ноль на 80 микросекунд (мксек), потом отпустить на те же 80 мксек — это можно считать подтверждением того, что датчик на линии живой и откликается: ^ 80ш ч р V / 1' ш **** ъ Щ W / V \ Start sending Host signal Slave signal После этого, сразу же, по падению с высокого уровня на нижний датчик начи- нает передавать 40 отдельных бит. Каждый бит кодируются специальной посылкой, которая состоит из двух интервалов. Сначала датчик занимает линию (кладет ее в ноль) на определенное время — своего рода первый «полубит». Потом датчик отпускает линию (линия подтягивается к единице) тоже на определенное время — это типа второй «полубит». Длительность этих интервалов — «полубитов» в мик- росекундах кодирует что собственно пытается передать датчик: бит "ноль" или бит ллединица". Рассмотрим описание битовой посылки: первый «полубит» всегда низкого уровня
и фиксированной длительности — около 50 мксек. Длительность второго «полуби- та» определят, что датчик собственно передает. Для передачи нуля используется сигнал высокого уровня длительностью 26-28 мксек: VDD GND 50vs 26 ■«-28»» \ К Host signal Slave signal Bit data "0м bit format Для передачи единицы, длительность сигнала высокого увеличивается до 70 микросекунд: VDD CND V \ \ Ж 9DU *- ^ W к \ i i \ rff Wat w. ^ ^ i h V 'Host signal Slave signal Bit data МГ bit format Мы не будет точно высчитывать длительность каждого интервала, нам вполне достаточно понимания, что если длительность второго «полубита» меньше чем первого — то закодирован ноль, если длительность второго «полубита» больше — то закодирована единица. Всего у нас 40 бит, каждый бит кодируется двумя им- пульсами, всего нам надо значит прочитать 80 интервалов. После того как про- читали 80 интервалов будем сравнить их попарно, первый ЛЛполубит" со вторым. Вроде все просто, что же требуется от микроконтроллера, для того чтобы про- читать данные с датчика? Получается, нужно значит дернуть ногой в ноль, а по- том просто считать всю длинную посылку с датчика на той же ноге. По ходу, бу- дем разбирать посылку на «полу-биты», определяя, где передается бит ноль, где единица. Потом соберем получившиеся биты, в байты, которые и будут ожидаемыми данными о влажности и температуре. Ок, мы начали писать код и для начала попробуем проверить, а работает ли вообще датчик, для этого мы просто положим линию на 20 мсек и посмотрим на линии, что из этого получится логическим анализатором. Как я уже писал сам датчик подключен на 2 ногу порта D. В Arduino Uno это цифровой выход D2 (смотрим для проверки Arduino Pinout). Все делаем тупо: инициализировали порт на выход, выставили ноль, подождали 20 миллисекунд, освободили линию, переключили ногу в режим чтения и ждем по- явление сигналов на ноге.
1 2 3 г 5 6 S 9 1С 11 12 13 К 15 16 17 IS DEFINES ; определения для порта., к. которому г .EQU .EQU .EQU .EQU .EQU .DEF .DEF .DEF .DEF .DEF .DEF .DEF .DEF .DEF .DEF подключем DHT11 DHT_Port=PORTD DHT_InPort=PIND DHT_Pin=P0RTD2 DHT_Direction=DDRD DHT_Direction_Pin=DDD2 Tmpl=R16 USART_ByteR=R17 Tmp2=R18 USART_BytesN=R19 Tmp3=R20 Cycle_Count=R21 ERR_CODE=R22 N_Cycles=R23 ACCUM=R24 Tmp4=R25 ; переменная для отправки байта ■ ; переменная - сколько байт отпр. ; счетчик циклов в Expect_X ; возврат ошибок из подп ; счетчик в READJIYCLES S 9 1С 11 12 13 15 16 17 18 19 22 21 22 23 2^ 25 26 27 23 ; после инициализации сразу !1!! надо считать ответ контроллера и собственно данные DHT_INIT: С LI ; еде раз, на всякий случай - критичная ко времени секция ; сохранили X для использования в READ_CYCLES - там нет времени LDI ХН, High(CYCLES) ; загрузили старший байт адреса Cycles LDI XL, Low (CYCLES) ; загрузили младший байт адреса Cycles LDI Tmpl, (l«DHT_Direction_Pin> OUT DHT_Direction, Tmpl LDI Tmpl, (8<<DHT_Pin) OUT DHT_Port, Tmpl RCALL DELAY_29MS LDI Tmpl, <K<DHT_Pin) OUT DHT_Port, Tmpl RCALL DELAY 19US ; порт D, Пин 2 на выход ; выставили 0 ; ждем 26 миллисекунд ; освободили линию - выставили 1 ; ждем 18 микросекунд LDI Tmpl, (9<<DHT_Direction_Pin) ; порт D, Pin 2 OUT DHT_Direction, Tmpl LDI Tmpl,(K<DHT_Pin) ; подтянули pull-up вход на вмес OUT DHT_Port, Tmpl ; ждем ответа от сенсора - он должен положить линик: в ноль на 80 us и отпустить на 80 us
Смотрим анализатором — а ответил ли датчик? Да, ответ есть — вот те сигналы после нашего первого импульса в 20 милсек — это и есть ответ датчика. Для просмотра посылки я использовал китайский клон USBEE AX Pro который подключен к сигнальному проводу датчика. 4mi/dfo л *Л9г-; ::s-ts *;s"r'. ::s't'. •*:ь*~: *s.:s--5 ::$'—, Oigit.il 0 1IIIIB Растянем масштаб так чтобы увидеть окончание нашего импульса в 20 мсек и лучше увидеть начало посылки от датчика — смотрим все как в datasheet — сна- чала датчик выставил низкий/высокий уровень по 80 мксек, потом начал переда- вать биты — а данном случае во втором «полубите» передается «0». 300us/div '9.53Г:5~1$ 9.s3**:$~s :;i3r:5-s Digital 0 ffl Значит, датчик работает и данные нам прислал, теперь надо эти данные пра- вильно прочитать. Поскольку задача у нас учебная, то и решать ее будем тупо в лоб. В момент ответа датчика, т.е. в момент перехода с высокого уровня в низ- кий, мы запустим цикл с счетчиком числа повторов нашего цикла. Внутри цикла, будем постоянно следить за уровнем сигнала на ноге. Итого, в цикле будем ждать, когда сигнал на ноге перейдет обратно на высокий уровень — тем самым определив длительность сигнала первого «полубита». Наш микроконтроллер рабо- тает на частоте 16 MHz и за период, например, в 50 микросекунд контроллер ус- пеет выполнить около 800 инструкций. Когда на линии появится высокий уровень — то мы из цикла аккуратно выходим, а число повторов цикла, которые мы отсчи- тали с использованием счетчика — запоминаем в переменную. После перехода сигнальной линии уже на высокий уровень мы делаем такую же операцию - считаем циклы, до момента когда датчик начнет передавать следующий бит и положит линию в низкий уровень. К счастью, нам не надо знать точный временной интервал наших импульсов, нам достаточно понимать, что один интер- вал больше другого. Понятно, что если датчик передает бит «ноль» то длитель- ность второго «полубита» и соответственно число циклов, которые мы отсчитали будет меньше чем длительность первого «полубита». Если же датчик передал бит «единица», то число циклов которые мы насчитаем во время второго полубита бу- дет больше чем в первым. И для того, что бы мы не висели вечно, если вдруг датчик не ответил или за- сбоил, сам цикл мы будем запускать на какой-то временной период, но который гарантированно больше самой длинной посылки, чтоб если датчик не ответил, то мы смогли выйти по тайм-ауту. В данном случае показан пример для ситуации, когда у нас на линии был ноль, и мы считаем сколько раз мы в цикле мы считали состояние ноги контроллера, пока датчик не переключил линию в единицу. Аналогичная подпрограмма используется для того, чтобы посчитать сколько циклов у нас должно прокрутиться, пока датчик из состояния ноль на линии пе- реложил линию в состояние единицы.
1 -I 3 L 5 6 7 о 9 .с .1 -> .0 .5 .6 . / .3 .9 1С [1 ■2 :£ : с EXPECT 1 ; крутимся в цикле ; когда появилось ; сообщаем сколько ; или сообщение ЕХРЕСТ_1: EXP1L1: ЕХ1Т_ЕХРЕСТ_1: об ждем нужного состояния на пине - выходим циклов ждали ошибке тайм оута если не дождались LDI Cycle_Count, 0 ; загрузили счетчик, циклов LDI ERR_CODE, 2 ; Ошибка 2 - выход по тайм Out ldi Trnpl., 2 ; Загрузили ldi Tmp2^ 169 ; задержку 80 us INC Cycle Count ; увеличили счетчик циклов IN ТтрЗ,, DHT_InPort ; читаем порт SBRC ТтрЗ., DHT_Pin ; Если 1 R3MP EXIT_EXPECT_1 ; To выходим dec Tmp2 ; если нет то крутимся в задержке brne EXP1L1 dec Trnpl brne EXP1L1 NOP ; Здесь выход по тайм out RET LDI ERR_C0DEj 1 ; ошибка 1, все нормально, в Cycle_Count RET Для расчета временных задержек мы будет использовать тот же подход, который мы использовали при мигании светодиодом — подберем параметры пустого цикла для формирования нужной паузы. Я использовал специальный калькулятор. При же- лании можно посчитать число рабочих инструкций и вручную. Памяти в нашем контроллере довольно много — аж 2 килобайта, так что мы не будем жлобствовать с памятью, и тупо сохраним данные счетчиков относительно наших 80 ( 40 бит, 2 интервала на бит) интервалов в память. Объявим переменную CYCLES: .byte 80 ; буфер для хранения числа циклов И сохраним все считанные циклы в память. 1 2 3 4 5 6 3 9 10 11 12 13 * ; читаем READ_ READ: биты .CYCLES: :_ KtAD CYLLtb = контроллера и сохраняем в LDI N_Cycles, 80 NOP RCALL EXPECT_1 ST Х+, Cycles_Counter RCALL EXPECT_0 ST Х-ь., Cycles_Counter DEC N_Cycles BRNE READ RET Cycles » У У у у У читаем 80 циклов Открутился 0 Сохранили число циклов Сохранили число циклов уменьшили счетчик все циклы считали
Теперь, для отладки, попробуем посмотреть насколько удачно посчиталось дли- тельность интервалов, и понять действительно ли мы считали данные из датчика. Понятно, что число отсчитанных циклов первого «полубита» должно быть примерно одинаково у всех битовых посылок, а вот число циклов при отсчете второго «по- лубита» будет или существенно меньше, или наоборот существенно больше. Для того чтобы передавать данные в большой компьютер будем использовать USART контроллера, который через USB кабель будет передавать данные в про- грамму — терминал, например PuTTY. Передаем опять же тупо в лоб — засовываем байт в нужный регистр управления USART-a и ждем, когда он передастся. Для удобства я также использовал пару подпрограмм, типа — передать несколько байт, начиная с адреса в Y, ну и перевести каретку в терминале для красоты. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 SEND. SEND. SEND. » SEND. SBSJ .BYTE: _BYTE_ .CRLF: - что .BYTES .1: LI: слать : QEMH 1 DVTC Х/ТЛ IICADT itNL» 1 DT 1 С VIA UiAK 1 NOP LDS Tmpl, UCSR0A SBRS Tmpl, UDRE0 RJMP SEND_BYTE_L1 STS UDR0, USART_ByteR NOP RET QPMri /"Dl P WTA IIQADT ->tNU LKLr VIA UbAK 1 LDI USART_ByteR, $0D RCALL SEND_BYTE LDI USART_ByteR, $0A RCALL SEND_BYTE RET ССМГ» M RVTPC \/TA IICADT ^tl\JU N DT1lj VIM UbAK1 , USART_BytesN - сколько байт NOP LD (JSART_ByteR, Y+ RCALL SEND_BYTE DEC USART_BytesN BRNE SBS_L1 RET ; если регистр данных пустой ; то шлем байт из R17 Отправив в терминал число отсчётов для 80 интервалов, можно попробовать со- брать собственно значащие биты. Делать будем, как написано в учебнике, т.е. в datasheet — попарно сравним число циклов первого «полубита» с числом циклов второго. Если вторые пол-бита короче — значит это закодировать ноль, если длиннее — то единица. После сравнения биты накапливаем в аккумуляторе и со- храняем в память по-байтово начиная с адреса BITS.
1 2 3 £ 5 с 3 9 1С 11 12 13 1£ 15 16 17 18 19 2С 21 22 23 2^ » ; Из Cycles GET_BITS: АСС: ТО_АСС: ]_SHIFT: лет 43 С 1 делаем РТТС Dl 1 2> байты в BITS LDI LDI LDI LDI LDI LDI LDI LDI LSL LD " LD ' CP " Tmpl., 5 Tmp2., 3 ZH, High(CYCLES) ZL, Loiv (CYCLES) YH, High(BITS) YL, Low (BITS) ACCUM, 9 Tmp2, 8 ACCUM ГглрЗ, Z+ Гглр4, Z+ ГглрЗ, Tmp4 BRPL ]_SHIFT ORI DEC ACCUM, 1 Tmp2 BRNE TO_ACC ST Y+, ACCUM DEC Tmpl BRNE ACC RET ; загру ; загру ; загру ; загру ; если ; для пяти байт - готовим счетчи ; для каждого бита зили старший байт адреса Cycles зили младший байт адреса Cycles зили старший байт адреса BITS зили младший байт адреса BITS ; акамулятор инициализировали ; для каждого бита ; сдвинули влево ; считали данные [i] ; о циклах и [i+1] ; сравнить первые пол бита с вто положительно (9) то просто сдвиг ; если отрицательно (1) то добав ; повторить для 8 бит ; сохранили акамулятор ; для пяти байт Итак, здесь мы собрали в памяти начиная с метки BITS те пять байт, которые передал контроллер. Но работать с ними в таком формате не очень неудобно, по- скольку в памяти это выглядит примерно, как: 34002100ХХ, где 34 — это влажность целая часть, 00 — данные после запятой влажности, 21 — температура, 00 — опять данные после запятой температуры, XX — контрольная сумма. А нам надо бы вывести в терминал красиво типа «Temperature = 21.00». Так что для удобства, растащим данные по отдельным пе- ременным . 1 2 3 4 Н1в: HBI- TIO Т91 .byte l .byte 1 .byte 1 .byte 1 ; число • ; число ■ ; число ; число - целая часть влажность - дробная часть влажность - целая часть температура в С - дробная часть температура И сохраняем байты из BITS в нужные переменные
1 2 3 4 5 6 7 3 9 10 11 12 13 14 15 16 17 13 19 20 21 22 23 24 9 ' 9 * из i i GET_ BITS i чуть чл: i nm l'mim — вытаскиваем цифры Н16 хаки HnT_DATA: ули, NOP потому что Н10 и дальше... лежат LDI ZH, HIGH(BITS) LDI ZL, LOU'(BITS) LDI XH, HIGH(H10) LDI XL, LOW(H10) LD Tmpl, Z+- ST X+, Tmpl LD Tmpl, Z-k ST X+, Tmpl LD Tmpl, Z* ST X-»-, Tmpl LD Tmpl, Z+- ST X+, Tmpl RET последовательно в памяти ; Считали ; сохранили ; Считали ; сохранили ; Считали ; сохранили ; Считали ; сохранили После этого преобразуем цифры в коды ASCII, чтобы данные можно было нор- мально прочитать в терминале, добавляем названия данных, ну там «температура» из флеша и шлем в СОМ порт в терминал. Для того, чтобы это измерять температуру регулярно, добавляем вечный цикл с задержкой порядка 1200 миллисекунд, поскольку datasheet DHT11 говорит, что не рекомендуется опрашивать датчик чаще чем 1 раз в секунду. Основной цикл после этого выглядит примерно так:
1 ;============ MAIN 2 ;1!1 Главный вход 3 RESET: NOP 4 5 ; Internal Hardware Init 6 СLI ; нам прерывания не нужны пока 7 8 ; stack init 9 LDI Tmpl, Low(RAMEND) 10 OUT SPL, Tmpl 11 LOI Tmpl, High(RAMEND) 12 OUT SPH, Tmpl 13 14 RCALL USART0_INIT 15 16 ; Init data 17 RCALL COPY_STRINGS ; скопировали данные в RATI 18 RCALL TEST_DATA ; подготовили тестовые данные 19 20 loop: NOP ; крутимся в веч! 21 ; External Hardware Init 22 RCALL DHT_INIT 23 ; получили здесь подтверждение контроллера и надо в темпе читать 24 RCALL READ_CYCLES 25 ; критичная ко времени секция завершилась... 26 27 ;Тест - отправить Cycles в USART 28 ;RCALL TEST_CYCLES 29 30 ; получаем из посыпки биты 31 RCALL <5ET_BITS 32 33 ;Тест - отправить BITS в USART 34 ;RCALL TEST_BITS 35 36 ; получаем из BITS цифровые данные 37 RCALL GET_HnT_DATA 38 39 ;Тест - отправить 4 байта начиная с Н10 в USART 40 ;RCALL TEST_H10_T01 41 42 ; подготовидли температуру и влажность в ASCII 43 RCALL HnT_ASCII_DATA_EX 44 45 ; Отправить готовую температуру (надпись и ASCII данные) в USART 46 RCALL PRINT_TEMPER 47 ; Отправить готовую влажность (надпись и ASCII данные) в USART 48 RCALL PRINT_HUMID 49 ; переведем строку дял красоты 50 RCALL SEND_CRLF 51 52 RCALL DELAY_1200MS ;повторяем кажды> 53 rjmp loop ; зациклились
Прошиваем, подключаем USB-TTL кабель (преобразователь)к компьютеру, запус- каем терминал, выбираем правильный виртуальный СОМ порта и наслаждаемся нашим новым цифровым термометром. Для проверки можно погреть датчик в руке — у меня температура при этом растет, а влажность, как ни странно, уменьшается. (ПРОДОЛЖЕНИЕ СЛЕДУЕТ)