Ich versuche, mit einem ATtiny13A ein ferngesteuertes RGB-LED-Licht herzustellen.
Ich weiß, dass der ATtiny85 für diesen Zweck besser geeignet ist, und ich weiß, dass ich möglicherweise nicht in der Lage sein werde, den gesamten Code anzupassen, aber im Moment ist mein Hauptanliegen, eine Software-PWM mit Interrupts im CTC-Modus zu generieren.
Ich kann in keinem anderen Modus arbeiten (außer Fast PWM mit OCR0A
as TOP
, was im Grunde dasselbe ist), da der von mir verwendete IR-Empfängercode eine 38-kHz-Frequenz benötigt, die er mit CTC und generiert OCR0A=122
.
Also versuche ich (und ich habe Leute gesehen, die dies im Internet erwähnt haben), die Interrupts Output Compare A
und zu verwenden Output Compare B
, um eine Software-PWM zu erzeugen.
OCR0A
, das auch vom IR-Code verwendet wird, bestimmt die Frequenz, die mir egal ist. Und OCR0B
, bestimmt den Arbeitszyklus des PWM, den ich zum Ändern der LED-Farben verwenden werde.
Ich erwarte, dass ich eine PWM mit einem Arbeitszyklus von 0-100% erhalten kann, indem ich den OCR0B
Wert von 0
auf ändere OCR0A
. Dies ist mein Verständnis dessen, was passieren sollte:
Aber was tatsächlich passiert, ist Folgendes (dies ist aus der Proteus ISIS-Simulation):
Wie Sie unten sehen können, kann ich einen Arbeitszyklus von etwa 25% bis 75% erreichen, aber für ~0-25% und ~75-100% bleibt die Wellenform einfach hängen und ändert sich nicht.
GELBE Linie: Hardware-PWM
ROTE Linie: Software-PWM mit festem Tastverhältnis
GRÜNE Linie: Software-PWM mit variierendem Arbeitszyklus
Und hier ist mein Code:
#ifndef F_CPU
#define F_CPU (9600000UL) // 9.6 MHz
#endif
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
int main(void)
{
cli();
TCCR0A = 0x00; // Init to zero
TCCR0B = 0x00;
TCCR0A |= (1<<WGM01); // CTC mode
TCCR0A |= (1<<COM0A0); // Toggle OC0A on compare match (50% PWM on PINB0)
// => YELLOW line on oscilloscope
TIMSK0 |= (1<<OCIE0A) | (1<<OCIE0B); // Compare match A and compare match B interrupt enabled
TCCR0B |= (1<<CS00); // Prescalar 1
sei();
DDRB = 0xFF; // All ports output
while (1)
{
OCR0A = 122; // This is the value I'll be using in my main program
for(int i=0; i<OCR0A; i++)
{
OCR0B = i; // Should change the duty cycle
_delay_ms(2);
}
}
}
ISR(TIM0_COMPA_vect){
PORTB ^= (1<<PINB3); // Toggle PINB3 on compare match (50% <SOFTWARE> PWM on PINB3)
// =>RED line on oscilloscope
PORTB &= ~(1<<PINB4); // PINB4 LOW
// =>GREEN line on oscilloscope
}
ISR(TIM0_COMPB_vect){
PORTB |= (1<<PINB4); // PINB4 HIGH
}
Eine minimale Software-PWM könnte so aussehen:
volatile uint16_t dutyCycle;
uint8_t currentPwmCount;
ISR(TIM0_COMPA_vect){
const uint8_t cnt = currentPwmCount + 1; // will overflow from 255 to 0
currentPwmCount = cnt;
if ( cnt <= dutyCyle ) {
// Output 0 to pin
} else {
// Output 1 to pin
}
}
Ihr Programm stellt dutyCycle
auf den gewünschten Wert ein und der ISR gibt das entsprechende PWM-Signal aus. dutyCycle
ist a uint16_t
, um Werte zwischen 0 und 256 einschließlich zuzulassen; 256 ist größer als jeder mögliche Wert von currentPwmCount
und bietet somit volle 100 % Einschaltdauer.
Wenn Sie 0 % (oder 100 %) nicht benötigen, können Sie einige Zyklen mit a kürzen uint8_t
, sodass entweder 0
ein Arbeitszyklus von 1/256 und 255
100 % oder 0
0 % und 255
ein Arbeitszyklus von 255/ 256.
Sie haben immer noch nicht viel Zeit in einer 38-kHz-ISR; Mit einem kleinen Inline-Assembler können Sie die Zykluszahl des ISR wahrscheinlich um 1/3 bis 1/2 reduzieren. Alternative: Führen Sie Ihren PWM-Code nur bei jedem zweiten Timer-Überlauf aus und halbieren Sie die PWM-Frequenz.
Wenn Sie mehrere PWM-Kanäle haben und die Pins, die Sie PMW-ing sind, alle auf dem gleichen PORT
sind, können Sie auch die Zustände aller Pins in einer Variablen sammeln und sie schließlich in einem Schritt an den Port ausgeben, der dann nur das Auslesen benötigt. port, and-with-mask, or-with-new-state, einmal auf Port schreiben statt einmal pro Pin/Kanal .
Beispiel:
volatile uint8_t dutyCycleRed;
volatile uint8_t dutyCycleGreen;
volatile uint8_t dutyCycleBlue;
#define PIN_RED (0) // Example: Red on Pin 0
#define PIN_GREEN (4) // Green on pin 4
#define PIN_BLUE (7) // Blue on pin 7
#define BIT_RED (1<<PIN_RED)
#define BIT_GREEN (1<<PIN_GREEN)
#define BIT_BLUE (1<<PIN_BLUE)
#define RGB_PORT_MASK ((uint8_t)(~(BIT_RED | BIT_GREEN | BIT_BLUE)))
uint8_t currentPwmCount;
ISR(TIM0_COMPA_vect){
uint8_t cnt = currentPwmCount + 1;
if ( cnt > 254 ) {
/* Let the counter overflow from 254 -> 0, so that 255 is never reached
-> duty cycle 255 = 100% */
cnt = 0;
}
currentPwmCount = cnt;
uint8_t output = 0;
if ( cnt < dutyCycleRed ) {
output |= BIT_RED;
}
if ( cnt < dutyCycleGreen ) {
output |= BIT_GREEN;
}
if ( cnt < dutyCycleBlue ) {
output |= BIT_BLUE;
}
PORTx = (PORTx & RGB_PORT_MASK) | output;
}
Dieser Code ordnet das Tastverhältnis einem logischen 1
Ausgang an den Pins zu; Wenn Ihre LEDs eine "negative Logik" haben (LED an, wenn der Pin niedrig ist ), können Sie die Polarität des PWM-Signals umkehren, indem Sie einfach if (cnt < dutyCycle...)
auf wechseln if (cnt >= dutyCycle...)
.
if
in die Interrupt-Routine einfügen, um nur den PWM-Code jedes zweite Mal auszuführen. Wenn mein PWM-Code zu lange dauert und der nächste Überlauf-Interrupt verpasst wird, ist mein Programm in Ordnung, da der nächste Interrupt sowieso nichts bewirken würde. Ist es das, was du meintest?Wie @JimmyB kommentierte, ist die PWM-Frequenz zu hoch.
Es scheint, dass die Interrupts eine Gesamtlatenz von einem Viertel des PWM-Zyklus haben.
Bei Überlappung ist das Tastverhältnis fest vorgegeben durch die Gesamtlatenzzeit, da der zweite Interrupt in die Warteschlange gestellt und ausgeführt wird, nachdem der erste verlassen wurde.
Das minimale PWM-Tastverhältnis ist durch den Prozentsatz der gesamten Interrupt-Latenz in der PWM-Periode gegeben. Die gleiche Logik gilt für das maximale PWM-Tastverhältnis.
Wenn man sich die Diagramme ansieht, liegt das minimale Tastverhältnis bei etwa 25 %, und dann muss die Gesamtlatenz ~ 1/(38000*4) = 6,7 µs betragen.
Folglich beträgt die minimale PWM-Periode 256*6,7 µs = 1715 µs und 583 Hz maximale Frequenz.
Einige weitere Erläuterungen zu möglichen Patches mit hoher Frequenz:
Der Interrupt hat zwei blinde Fenster, wenn nichts getan werden kann, und tritt in end ein und verlässt den Interrupt, wenn der Kontext gespeichert und wiederhergestellt wird. Da Ihr Code ziemlich einfach ist, vermute ich, dass dies einen guten Teil der Latenzzeit in Anspruch nimmt.
Eine Lösung zum Überspringen der niedrigen Werte wird immer noch eine Latenz haben, die mindestens so groß ist wie das Verlassen des Interrupts und das Eintreten in den nächsten Interrupt, sodass das minimale Tastverhältnis nicht wie erwartet ist.
Solange dies nicht weniger als ein PWM-Schritt ist, beginnt das PWM-Tastverhältnis bei einem höheren Wert. Nur eine leichte Verbesserung gegenüber dem, was Sie jetzt haben.
Ich sehe, dass Sie bereits 25 % der Prozessorzeit in Interrupts verwenden, also warum verwenden Sie nicht 50 % oder mehr davon, lassen den zweiten Interrupt und sammeln nur für das Vergleichs-Flag. Wenn Sie nur Werte bis 128 verwenden, haben Sie nur bis zu 50 % Arbeitszyklus, aber mit der Latenz von zwei Befehlen, die in Assembler optimiert werden könnten.
Ahorn
Pouria P
OCR0A
wird vom IR-Code verwendet, also habe ich nurOCR0B
. Ich versuche, damit Software-PWM auf 3 Nicht-PWM-Pins zu erzeugen.JimmyB
Pouria P
JimmyB
JimmyB
Pouria P
JimmyB
Ahorn
Pouria P