ATtiny85 - 带 Timer1 的软件 UART

Posted

技术标签:

【中文标题】ATtiny85 - 带 Timer1 的软件 UART【英文标题】:ATTiny85 - Software UART with Timer1 【发布时间】:2020-07-14 19:00:08 【问题描述】:

所以最近我尝试为 ATtiny85 实现软件 UART(仅限 TX)。我想用内部的Timer1来驱动它。

定时器应以波特率的频率中断。每个 ISR 都会发送一位,直到没有任何内容可发送,中断将再次被禁用。

(注意:F_CPU=1000000 ;保险丝是出厂默认设置(E:FF、H:DF、L:62))

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdint.h>

#define SET(reg, pos) (reg |= 1<<(pos))
#define FLP(reg, pos) (reg ^= 1<<(pos))
#define CLR(reg, pos) (reg &= ~(1<<(pos)))
#define GET(reg, pos) (reg &  1<<(pos))

#define UART_TX_BIT PB1
#define UART_BAUDRATE 600

static volatile uint16_t txframe;

/* Timer1A interrupt at BAUDRATE */
ISR(TIM1_COMPA_vect)

        /* Write current bit */
        if(txframe & 1) SET(PORTB, UART_TX_BIT);
        else            CLR(PORTB, UART_TX_BIT);

        /*
         * If the 1 mark at the end of txframe is reached,
         * disable interrupts (stop transmitting)
         */
        if(txframe == 1) CLR(TIMSK, OCIE1A);

        txframe >>= 1;


static void uart_putc(const char c)

        /* Wait until pending txframe is transmitted */
        do 
                sei();
                __asm__ __volatile__ ("nop");
                cli();
        while(txframe);

        /* MARK . STOP | DATA | START */
        txframe = (0b11<<9) | ((uint16_t) c<<1) | 0;

        /* Enable timer interrupt and clear flag */
        SET(TIMSK, OCIE1A);
        SET(TIFR, OCF1A);
        sei();


static void uart_init()

        uint8_t sreg = SREG;

        cli();

        /* Set timer1 (CK) to CTC with divisor of 8 */
        TCCR1 = _BV(CTC1) | _BV(CS12);

        /* Set BAUDRATE clock divisor */
        OCR1A = (uint8_t) ((uint32_t) (F_CPU/8)/UART_BAUDRATE)-1;

        /* Enable and pull TX Pin to HIGH */
        SET(DDRB, UART_TX_BIT);
        SET(PORTB, UART_TX_BIT);

        txframe = 0;

        SET(TIFR, OCF1A);
        sreg = SREG;


int main()

        uart_init();

        for(;;) 
                uart_putc('A');
                _delay_ms(2000);
        


使用此设置,接收器仅收到 0x00 或 0xFF,偶尔还会收到一些其他垃圾(取决于波特率)

最终我尝试在不中断的情况下实现相同的目标:

#define UART_FALLBACK_DELAY() _delay_us(1000000UL/UART_BAUDRATE)

static void uart_putc_fallback(uint8_t c)

        uint8_t sreg = SREG;
        cli();

        /* Start */
        CLR(PORTB, UART_TX_BIT);
        UART_FALLBACK_DELAY();
        /* Data */
        for(int i = 0; i < 8; i++, c>>=1) 
                if(c&1) SET(PORTB, UART_TX_BIT);
                else    CLR(PORTB, UART_TX_BIT);
                UART_FALLBACK_DELAY();
        
        /* Stop */
        SET(PORTB, UART_TX_BIT);
        UART_FALLBACK_DELAY();

        SREG = sreg;


static void uart_putc_fallback2(const char c)

        uint8_t sreg = SREG;
        cli();

        txframe = (0b11<<9) | ((uint16_t) c<<1) | 0;

        while(txframe) 
                if(txframe & 1) SET(PORTB,UART_TX_BIT);
                else            CLR(PORTB,UART_TX_BIT);
                txframe >>= 1;
                UART_FALLBACK_DELAY();
        

        SREG = sreg;

令人惊讶的是,这两个函数都按预期工作,所以我想我把 Timer1 搞砸了。遗憾的是我没有示波器,所以我无法手动检查信号。但总的来说,使用 Timer1 时信号似乎有点慢。设置应每 1664µs 中断一次:

波特 = 600Hz CK = 1MHz Timer1DIV = 8 Timer1CK = CK/Timer1DIV = 125kHz OCR1A = Timer1CK/波特 = 208 延迟 = (Timer1DIV * OCR1A)/CK = (8*208)/1MHz = 1664µs

谁能告诉我,为什么中断方法没有按预期工作?


更多信息:

Vcc = ~4.52V (~25°C) Oscillator 应该足够准确 Timer1 Prescale, Table 12-5 Timer1 Overview 默认情况下 Timer1 使用 CK (1MHz) 而不是 PCK(我已经尝试手动将其设置为 CK) 工具链:avr-gcc

【问题讨论】:

精心设计的问题!你已经尝试了所有我会先尝试的东西。代码乍一看还不错。我认为有些愚蠢。我也希望我有一个方便的范围,但我会看看我能不能用肉眼找到任何东西。 【参考方案1】:

我想我发现了一个可能导致您所看到的问题。

在 CTC 模式下,此计时器在达到OCR1C...后清除计数器...

您似乎没有在代码中设置OCR1C,因此计时器的周期将基于OCR1C 中发生的任何事情(即加电时的0)。

这很容易错过(OCR1COCR1A)!

我认为您可以通过在此处添加一行来设置此寄存器来使您的代码正常工作...

    /* Set BAUDRATE clock divisor */
    uint8_t match = (uint8_t) ((uint32_t) (F_CPU/8)/UART_BAUDRATE)-1;

    OCR1A = match;    // Generate interrupt on match to drive ISR 
    OCR1C = match;    // Clear counter on match to define period

如果是这样,请报告——如果不是,我们会继续寻找!

【讨论】:

哦,你完全正确!这使它工作,感谢您的回答。 @vipeout 提示:您可以将uart_putc() 中的do 块替换为简单的while(txframe);。由于变量是volatile,它会在循环运行时得到更新。额外的clisei 会导致您的位转换出现不必要的抖动。 好的,这将使代码更简洁。谢谢:) 由于txframe uint16_t 大于uint8_t,因此txframe 的内存读取不是原子的,我只会读取txframe 而写入txframe 的中断被禁用。类似于(对于启用正常中断的程序)while (1) cli(); const uint16_t txf = txframe; sei(); if (!txf) break;

以上是关于ATtiny85 - 带 Timer1 的软件 UART的主要内容,如果未能解决你的问题,请参考以下文章

attiny13a与attiny85区别

attiny85死机

[Ardunio] ATtiny85的时钟问题

推荐 Attiny85 编译器/IDE [关闭]

如何使用Arduino UNO开发板编程ATtiny85

如何在Arduino环境使用ATtiny13/44/45/84/85