What is PWM and how it works in detail I will not paint, information can easily be found on the Internet. I will touch only the general concepts. PWM is Pulse Width Modulation (in English PWM - Pulse Width Modulation) already from the very name it is clear that there is something related to pulses and their width. If you change the width (duration) of the pulses of constant frequency, you can control, for example, the brightness of the light source, the speed of rotation of the motor shaft or the temperature of a heating element. Usually, it is with the help of PWM that the microcontroller controls such a load. Microcontrollers have a hardware implementation of PWM, but unfortunately, the number of hardware PWM channels is limited, for example, there are six pieces in ATmega88, four in ATtiny2313, three in ATmega8, and only two in ATtiny13. In AVR, PWM channels use timers and their OCRxx comparison registers. Changing their contents and setting the parameters of the timers, depending on the tasks, you can control the state associated with the register, the output - submit it to 1 or 0. The same can be organized programmatically, controlling any controller output, and most importantly, to realize more PWM- channels than there is onboard hardware. In practice, the number of channels is limited only by the number of legs of the microcontroller (at least, if we talk about the Mega or Tiny families). As it turned out, the algorithm is quite simple, but it took me some time to understand and fully understand it.
This algorithm is detailed in the original Appnote AVR136: Low-Jitter Multi-Channel Software PWM. The principle of the software implementation is to simulate the operation of the timer in the PWM mode. The required pulse duration is set by variables, respectively, one for each channel (in my code lev_ch1, lev_ch2, lev_ch3), as well as the “twins” of these variables, which store the value for a specific timer operation period (in my code buf_lev_ch1, buf_lev_ch2, buf_lev_ch3). The eight-bit timer starts at the main MK frequency and generates an overflow interrupt, that is, every 256 clock cycles. This imposes a limitation on the duration of the interrupt handling procedure — you must be set to 256 clock cycles in order not to miss the next interrupt. As a result, one full PWM period equals 256 * 256 = 65536 ticks. The eight-bit counter variable (in my example, counter) increments each interrupt by one and acts as a position indicator within a PWM cycle. All this provides the resolution (minimum pitch) of PWM to 1/256, and the pulse frequency to ƒ / (256 * 256), where ƒ is the frequency of the microcontroller's master oscillator. It should be noted that the clock frequency of the microcontroller should be quite high. In my example, ATtiny13 operates at the highest possible frequency, without the use of an external oscillator - 9.6 MHz. This gives a PWM period of 9,600,000 / 65536≈146.5 Hz, which is quite sufficient in most cases.
C code, an example of the implementation of the idea for ATtiny13 MK (three PWM channels on the pins PB0, PB1, PB2):
#define F_CPU 9600000 //fuse LOW=0x7a #include <avr/interrupt.h> #include <util/delay.h> uint8_t counter=0; uint8_t lev_ch1, lev_ch2, lev_ch3; uint8_t buf_lev_ch1, buf_lev_ch2, buf_lev_ch3; void delay_ms(uint8_t ms) // { while (ms) { _delay_ms(1); ms--; } } int main(void) { DDRB=0b00000111; // PortB 0,1,2 TIMSK0 = 0b00000010; // TCCR0B = 0b00000001; // , sei(); // lev_ch1=0; // lev_ch2=64; // lev_ch3=128; // while (1) // { for (uint8_t i=0;i<255;i++) { lev_ch1++; // lev_ch2++; // lev_ch3++; // delay_ms(50); // 50 } } } ISR (TIM0_OVF_vect) // { if (++counter==0) // { buf_lev_ch1=lev_ch1; // buf_lev_ch2=lev_ch2; buf_lev_ch3=lev_ch3; PORTB |=(1<<PB0)|(1<<PB1)|(1<<PB2); // 1 } if (counter==buf_lev_ch1) PORTB&=~(1<<PB1); // 0 if (counter==buf_lev_ch2) PORTB&=~(1<<PB0); // if (counter==buf_lev_ch3) PORTB&=~(1<<PB2); // . }
I think everything is quite clear and explanation is superfluous. For duration values and their buffers, with a larger number of channels, it may be better to use arrays, but in this example, I did not do this, for the sake of greater clarity.
Tested on avr-gcc-4.7.1 and avr-libc-1.8.0. Compile and retrieve the firmware file:
avr-gcc -mmcu=attiny13 -Wall -Wstrict-prototypes -Os -mcall-prologues -std=c99 -o softPWM.obj softPWM.c
avr-objcopy -O ihex softPWM.obj softPWM.hex
For proper operation, you need to set low-order fuse-bits in 0x7a (frequency 9.6 MHz). in avrdude, for example, it is done like this:
avrdude -p t13 -c usbasp -U lfuse:w:0x7a:m
My version of the implementation of the assembler. The program does exactly the same thing as the previous C code.
; include- .list .equ DDRB= 0x17 .equ PORTB= 0x18 .equ RAMEND= 0x009f .equ SPL= 0x3d .equ TCCR0B= 0x33 .equ TIMSK0= 0x39 .equ SREG= 0x3f ; , .def temp=R16 .def lev_ch1=R17 .def lev_ch2=R18 .def lev_ch3=R19 .def buf_lev_ch1=R13 .def buf_lev_ch2=R14 .def buf_lev_ch3=R15 .def counter=R20 .def delay0=R21 .def delay1=R22 .def delay2=R23 .cseg .org 0 ; : rjmp RESET ; Reset Handler rjmp EXT_INT0 ; IRQ0 Handler rjmp PIN_CHG_IRQ ; PCINT0 Handler rjmp TIM0_OVF ; Timer0 Overflow Handler rjmp EE_RDY ; EEPROM Ready Handler rjmp ANA_COMP ; Analog Comparator Handler rjmp TIM0_COMPA ; Timer0 CompareA Handler rjmp TIM0_COMPB ; Timer0 CompareB Handler rjmp WATCHDOG ; Watchdog Interrupt Handler rjmp ADC_IRQ ; ADC Conversion Handler ;RESET: EXT_INT0: PIN_CHG_IRQ: ;TIM0_OVF: EE_RDY: ANA_COMP: TIM0_COMPA: TIM0_COMPB: WATCHDOG: ADC_IRQ: reti RESET: ldi temp,0b00000111 ; PortB PB0, PB1 out DDRB,temp ; PB2 ldi temp,0 ; out PORTB,temp ; PortB 0 ldi temp,low(RAMEND) ; out SPL,temp ; ldi temp,0b00000001 ; . out TCCR0B,temp ; ldi temp,0b00000010 ; . out TIMSK0,temp ; sei ; start_pwm: ; inc lev_ch1 ; inc lev_ch2 ; inc lev_ch3 ; rcall delay ; rjmp start_pwm delay: ; ldi delay2,$01 ; ldi delay1,$77 ; ldi delay0,$00 ; $017700 - 50 loop: subi delay0,1 ; sbci delay1,0 ; sbci delay2,0 ; brcc loop ret TIM0_OVF: ; push temp ; in temp,SREG ; temp SREG push temp inc counter ; 0 cpi counter,0 ; 0, brne ch1_off ; mov buf_lev_ch1,lev_ch1 ; 0 mov buf_lev_ch2,lev_ch2 ; mov buf_lev_ch3,lev_ch3 ; ldi temp,0b00000111 ; out PORTB,temp ; ch1_off: ; cp counter,buf_lev_ch1 ; ? brne ch2_off ; , - cbi PORTB,0 ; ch2_off: ; cp counter,buf_lev_ch2 ; ? brne ch3_off ; , - cbi PORTB,1 ; ch3_off: ; cp counter,buf_lev_ch3 ; ? brne irq_end ; , - cbi PORTB,2 ; , irq_end: ; pop temp ; SREG temp out SREG,temp pop temp reti ;
Compiled with avra or tavrasm. Do not forget about fuse-bits (see above).