AVR单片机教程——UART进阶

发布者:等放假的Lwj最新更新时间:2020-09-08 来源: eefocus关键字:AVR  单片机  UART进阶 手机看文章 扫描二维码
随时随地手机看文章

在第一期中,我们已经开始使用UART来实现单片机开发板与计算机之间的通信,但只是简单地讲了讲一些概念和库函数的使用。在这一篇教程中,我们将从硬件与软件等各方面更深入地了解UART。


USART组件

一直在讲的UART其实是USART组件的一部分,USART比UART多了同步的一部分,但这一部分用得太少(我从来没用过),而且缺乏实例,所以就略过了。然而,单片机的设计者很机智地把这个鸡肋功能升华了一下,USART组件可以支持SPI模式。SPI是一种同步串行总线,可以支持很高的传输速率。这个功能使得ATmega324PA支持最多3个SPI通道,其中一个是纯SPI,另两个就是SPI模式下的USART。我们将在下一讲中揭开SPI的神秘面纱。


回到UART模式下的USART组件。开发板引出的RX和TX引脚是属于USART0组件的,因此使用时以下n都用0代替。


UART共有5个寄存器:


UDRn是收发数据寄存器,收(RXB)和发(TXB)使用不同的寄存器,但都通过UDRn来访问。向TXB写入一个字节,UART就开始发送;RXB保存接收到的数据,带有额外一个字节的缓冲(如同下一节要讲的缓冲区)。


UCSRnA包含UART状态位,如三个中断对应的标志,以及一些不常用的设置位。


UCSRnB主要用于使能,包括收发器与三个中断的使能位,以及9位帧格式相关的位。


UCSRnC是最主要的控制寄存器,可以配置USART的模式与格式。


UBRRnL和UBRRnH(可以通过UBRRn来访问这个16位寄存器)用于设定波特率,在异步模式下,BAUD=fCPU16(UBRRn+1)。


UART支持三个中断,分别是接收完成(RX)、数据寄存器空(UDRE)、发送完成(TX)。第一个用于接收,后两个用于发送,一般使用UDRE。


RX中断允许程序在任何时刻及时地接收并处理总线上发来的数据。沿用串口接收一讲中的例子:


#include

#include

#include


int main(void)

{

    led_init();

    PORTD |=    1 << 0;      // RXD0 pull-up

    UCSR0B =    1 << RXCIE0  // RX interrupt

           |    1 << RXEN0   // RX enabled

           |    1 << TXEN0;  // TX enabled

    UCSR0C = 0b00 << UMSEL00 // asynchronous USART

           | 0b10 << UPM00   // even parity

           |    0 << USBS0   // 1 stop bit

           | 0b11 << UCSZ00; // 8-bit

    UBRR0L = 40;             // 38400bps

    sei();

    while (1)

        ;

}


ISR(USART0_RX_vect)

{

    static const char led_char[4] = {'r', 'y', 'g', 'b'};

    static uint8_t which = 4;

    uint8_t byte = UDR0;

    bool matched = false;

    for (uint8_t i = 0; i != 4; ++i)

        if (byte == led_char[i])

        {

            matched = true;

            which = i;

            break;

        }

    if (!matched && (byte == '0' || byte == '1'))

    {

        matched = true;

        if (which < 4)

            led_set(which, byte - '0');

        which = 4;

    }

    if (!matched)

        which = 4;

}

TX与UDRE中断允许程序在总线发送数据同时执行其他代码。比如,在打印ASCII表的同时控制LED闪烁。


#include

#include

#include

#include


int main(void)

{

    led_init();

    UCSR0B =    1 << UDRIE0  // UDRE interrupt

           |    1 << TXEN0;  // TX only

    UCSR0C = 0b00 << UMSEL00 // asynchronous USART

           | 0b10 << UPM00   // even parity

           |    0 << USBS0   // 1 stop bit

           | 0b11 << UCSZ00; // 8-bit

    UBRR0L = 40;             // 38400bps

    sei();

    while (1)

    {

        led_on();

        delay(500);

        led_off();

        delay(500);

    }

}


ISR(USART0_UDRE_vect)

{

    static char c = 0x21;

    UDR0 = c;

    if (++c == 0x7F)

        c = 0x21;

}

你看,不用定时器,只需总线中断与老套的main结合即可。


值得一提的是UDRE中断的设计特别人性化——UDREn的复位值是1,程序可以把所有数据都放在中断中,控制部分只需开关中断——而SPI和I²C组件都没有这个特性。至于它到底带来多少好处,只有在码的过程中体会了。


缓冲区

如果你较真一点,就会觉得上面这个程序很烂:


把硬件驱动(UART配置与中断)与业务逻辑(要输出的内容)紧紧地连接在一起(专业点讲,叫“紧耦合”),不符合可复用性等一系列设计原则;


ASCII表是十分有规律的,而大多数程序的输出则不然,需要UDRE中断以外的代码来决定要输出什么字符串,仅中断并不能解放常规的输出。


其实我们还遇到过其他问题:


相比25MHz的CPU频率,UART的38400波特率是很慢的,传输一个字节的时间可以让CPU执行几千条指令,但uart_print_string等函数的策略都是等待UART把数据发送完成才返回,是阻塞的;


uart_scan_string等函数要求程序乖乖地等待总线上的数据到来,不能错过,这使程序不能在等待的同时做其他事;


以上两点相结合更让人尴尬——在发送的同时接收到的数据会被错过,怎么还能叫全双工总线呢?


这输入和输出两方面的问题可以用一种高度对称的手段来解决,它就是缓冲区。缓冲区是这样一种结构,它存放着一串字符,来自于程序的输出或UART的接收,并可以按顺序取出,用于UART的发送或程序的输入。显然,这需要用到中断:在RX中断中,向缓冲区中放入接收到的数据;在UDRE中断中,如果缓冲区中有数据,则取出并发送之。


于是,当程序需要输入时,可以从缓冲区中取一些字符,并解析成整数等类型,如果缓冲区为空,则等待输入,与C语言标准输入scanf很类似;当程序需要输出时,可以直接把字符串写到缓冲区中,让中断来逐字节发送,而主程序可以无需等待,直接继续工作,这种输出是异步的。这个“异步”与UART总线的“异步”是不同的概念。关于阻塞、异步等概念,可参考:怎样理解阻塞非阻塞与同步异步的区别?


但是现在“缓冲区”还只是一个抽象概念,我们要把它落实成代码。如何实现一个缓冲区呢?


我们先把缓冲区想象成一个管道,有头和尾两端,我们需要从尾部放入球,从头部取出。这种数据结构称为队列。


队列可以用链表来实现,好处是队列的长度没有限制,除非内存耗尽。但是在我们的应用场景中,链表节点中有效的数据是一个字节,却还需要两个字节来存放一个指针,不太划算。并且,malloc函数是比较耗时的,应避免频繁调用。


我们使用一种叫作“循环队列”的实现。循环队列是一个数组,保存两个下标,分别指向头和尾(由于我主要写C++,我习惯用尾后)。循环体现在,假如队列的大小是64,那么下标为63的元素的后一个就是下标为0的元素。如果把普通数组想象成一个矩形,那么循环队列就是一个圆环。


初始时,头和尾下标相同。向尾部放入一个字节,就是在尾下标处写数据,并让尾下标指向下一个元素;取出一个字节,就是读取头下标处的数据,并让头下标指向下一个元素。当两个下标相等时,队列为空;当尾的后一个等于头时,队列满——可是明明这时只放了63个元素,为什么不再放一个呢?因为会与队列空的情况冲突,无法分辨,为了省事,还是浪费一个字节吧。


下面这段代码需要你认真阅读并理解,但是请先忽略volatile和ATOMIC_BLOCK(ATOMIC_FORCEON),当它们不存在就可以了。你也可以参考一些循环队列相关的资料来更好地理解这种结构(本来我想写的,但这篇已经很长了)。


#include

#include

#include

#include

#include


#define UART_TX_BUFFER_SIZE 64

#define UART_TX_BUFFER_MASK (UART_TX_BUFFER_SIZE - 1)


volatile char uart_tx_buffer[UART_TX_BUFFER_SIZE];

volatile uint8_t uart_tx_head = 0;

volatile uint8_t uart_tx_tail = 0;


void uart_init_buffered()

{

    UCSR0B =    0 << UDRIE0  // UDRE interrupt disabled

           |    1 << TXEN0;  // TX only

    UCSR0C = 0b00 << UMSEL00 // asynchronous USART

           | 0b10 << UPM00   // even parity

           |    0 << USBS0   // 1 stop bit

           | 0b11 << UCSZ00; // 8-bit

    UBRR0L = 40;             // 38400bps

}


void uart_print_char_buffered(char c)

{

    bool full = true;

    while (1)

    {

        ATOMIC_BLOCK(ATOMIC_FORCEON)

        {

            if (((uart_tx_tail + 1) & UART_TX_BUFFER_MASK) // 0->1, ..., 63->0

                != uart_tx_head)

                full = false;

        }

        if (!full)

            break; // if full, wait until buffer is not full

    }

    ATOMIC_BLOCK(ATOMIC_FORCEON)

    {

        if (uart_tx_head == uart_tx_tail)

            UCSR0B |= 1 << UDRIE0;

        uart_tx_buffer[uart_tx_tail] = c;

        uart_tx_tail = (uart_tx_tail + 1) & UART_TX_BUFFER_MASK;

    }

}


ISR(USART0_UDRE_vect)

{

    UDR0 = uart_tx_buffer[uart_tx_head];

    uart_tx_head = (uart_tx_head + 1) & UART_TX_BUFFER_MASK;

    if (uart_tx_head == uart_tx_tail)

        UCSR0B &= ~(1 << UDRIE0);

}

看到这里我默认你已经理解了循环数组,下面来看这些被忽略的语句。声明为volatile的变量一定会被放在内存中而不是通用寄存器中;ATOMIC_BLOCK的功能是,后面的大括号中的语句是原子的,在执行时不会被中断;ATOMIC_FORCEON会在执行完后把全局中断打开。


相信你一定对这种代码感到不适,为什么需要这么麻烦呢?以if (uart_tx_head == uart_tx_tail)这一句为例,这句语句通常由主程序执行。


假设执行到这一句前时uart_tx_head为41,uart_tx_tail为42,即缓冲区中还有1字节没有发送。


程序读取uart_tx_head,其值为41。


在读取uart_tx_tail之前,USART0_UDRE_vect中断触发了,在中断中最后一个字节被发送,uart_tx_head被修改为42,UDRIE0被写0,关掉了这个中断,随后中断退出。


程序读取uart_tx_tail,其值为42,两者不相等,UDRIE0不会被写1,中断保持关闭状态。


缓冲区中被写了一个字节,uart_tx_tail变为43。缓冲区明明非空,UDRE中断却没有开,这个字节无法发送。


这样分析很累,我写的时候并没有认真分析不加原子操作可能带来的问题,而是遵循这样的原则:对于非中断与中断的代码共享的数据,在非中断代码中一定要加原子,在中断代码中,如果在使用这些数据时全局中断可能处于打开状态,则也需要加原子。


现在我们实现了串口输出缓冲区,输入缓冲区的原理类似,留作作业。我们还需要关注几个问题:


串口输出是连续的字符流。“连续”是指不存在发送几个字节,停顿一下,再继续发送的情况;“字符流”是指发送的数据都是字符。在字符流的假设下,如果需要可以断开的输出,可以通过用标记断点来实现。但是对于字节流,即数据本身就可能包含的情形下,如何标记断点呢?作业4在缓冲区的基础之上增加了这样的需求。


以上代码对于在缓冲区满时插入字符的策略是等待直到缓冲区有空位,虽然一定能等到,保证数据被发送,但可能需要等待很长时间。比如,在缓冲区满时发送一个较长的字符串,插入每一字节时都需要等待一个字节被发送的时间,总体上与同步发送无异。这里提供几种方案:用一种结构来标记是否发生了错误,以及发生何种错误;给发送函数添加返回值,指示是否发送成功;使用动态缓冲区,当缓冲区满时新开辟一块空间存放。不过,还是要根据应用选择最合适的。


多路发送

UART是稀缺资源,单片机一共有两个,我设计的时候用掉一个,要是再加个串口调试,就用完了。但是,利用一个额外的GPIO和开发板左上角的逻辑门资源,我们可以把一个UART发送通道扩展成两个。


这个组合电路有两个输入:单片机UART输出(UART,简写为U)和信号选择(SEL,简写为S);两个输出:当SEL为低电平时有效的通道A(OUTA,简写为A)和当SEL为高电平时有效的通道B(OUTB,简写为B,以上名字都是随便起的)。这样,尽管不能在两个通道同时发送,但至少SEL可以控制每个字节的流向。


回顾UART的帧格式,当信号线上没有信号的时候,它是保持高电平的,因此对于A通道,当SEL为高时,OUTA总是为高;当SEL为低时,OUTA电平与UART相同,可以得到A=U+S,+号表示逻辑或。同理,B=U+S¯¯¯,上划线表示逻辑非。另外,·号表示逻辑与。


但是开发板上并没有或门和非门,只有与非门(|,C语言中|表示什么?)和或非门(↓),我们需要把这两个式子变形一下:


A=U+S=U¯¯¯¯⋅S¯¯¯¯¯¯¯¯¯¯¯¯¯¯=(U↓0)|(S↓0)


B=U+S¯¯¯=U¯¯¯¯⋅S¯¯¯¯¯¯¯¯¯¯¯=(U↓0)|S


这样我们就可以画出原理图:

左边两个是或非门,右边是与非门,分别位于开发板左上角标注NOR和NAND处。每个门有两个输入和一个输出,对应A、B、O三个引脚,A和B是可以对调的。


然后就要根据原理图搭建电路。也许你对这张并不复杂的电路图毫无头绪。的确,面包板上看似简单的电路也可能很复杂,不过还是有规则可以遵循的:


每一条杜邦线有两端,是黑色胶壳加上一根针或者没有针。有针的称为“公”,没有的称为“母”(我真没开车)。


板上的排针连接母头,面包板连接公头。


杜邦线有3种:公对公、公对母、母对母。


面包板上,一行5个孔是连接起来的。


各个引脚可以划分为若干不相交集合,相同集合内的引脚有导线连接,不同集合内的引脚没有引脚连接。每个集合称为一个net。


对于只有一个引脚的net,不管它。


对于有两个引脚的net,选用合适的杜邦线把两个引脚直接连接。


对于有至少3个引脚的net,通常需要借助面包板,选用合适的杜邦线把每个引脚与面包板上同一行连接。


这张图里只有SEL和第一个或非门的输出这两个net有3个引脚,因此面包板上只会有6根线,像这样:


最后简单地测试一下,PIN_D用作SEL。


#include

#include

#include


int main(void)

{

    pin_mode(PIN_D, OUTPUT);

    uart_init(UART_TX);

    for (int16_t i = 0; ; ++i)

    {

        pin_write(PIN_D, i & 1);

        uart_print_int(i);

        uart_print_line();

        delay(500);

    }

}


把两个通道连接到USB转串口工具上,分别可以看到奇数和偶数的输出。

关键字:AVR  单片机  UART进阶 引用地址:AVR单片机教程——UART进阶

上一篇:AVR单片机教程——矩阵键盘
下一篇:AVR单片机教程——LCD1602

推荐阅读最新更新时间:2024-11-12 11:31

单片机GPS定位LCD1602液晶显示经纬度海拔及时间日期实物制作
制作出来的实物图如下: 单片机源程序如下: #include main.h #include LCD1602.h #include GPS.h //定义变量 unsigned char KEY_NUM = 0; bit Page = 0; unsigned char xdata Display_GPGGA_Buffer ; unsigned char xdata Display_GPRMC_Buffer ; bit Flag_OV = 0; bit Flag_Calc_GPGGA_OK = 0; bit Flag_Calc_GPRMC_OK = 0; //****************************
[单片机]
<font color='red'>单片机</font>GPS定位LCD1602液晶显示经纬度海拔及时间日期实物制作
STM32单片机上电后时钟的默认配置过程
写作原由:今日接手用stm32f100xx芯片开发的项目,以前用的是stm8s 和stm32f103xx芯片;因为在别人的项目代码的基础上做2次开发,但是发现那个代码main函数中没有对系统时钟的设置的相关函数,一直纳闷,但也没有深究,直至昨日 调试时出现串口收发数据出错,源代码在原项目的板子上串口发送、接收数据正常,同样程序在项目板子上收发的数据不正确, 两块板子芯片一样,串口收发管脚一样,最后发现原来板子外部晶振是8MHZ ,新板子外部晶振是12MHZ; 而在STM32固件库中,默认的外部晶振是8MHZ,由于时钟源不正确,导致波特率不正确,当然收发的数据也不正确了…..我勒个去!都怪自己平时看问题“不求甚解”。 波特率与
[单片机]
STM32<font color='red'>单片机</font>上电后时钟的默认配置过程
51单片机-蜂鸣器原理
1.单片机IO端口电流 在讲解蜂鸣器之前我们还需拿LED硬件连接做另一个知识讲解,假如我们的LED这样接 此时即使单片机IO端口输出高电平5V,灯的亮度是很低的,因为单片机IO端口流出来的电流太少,无法驱动LED正常发光,大家不要停留在中学的物理常识中认为电压电阻都一样电流就一样了,这个是关联到单片机内部的集成电路原因的,这里请大家今后积累一些关于驱动负载的一些知识,也可参考《手把手教你学51单片机》文档3.3.3节和9.2节。 但是如果这样接 这时单片机IO端口输出低电平时灯却很亮,原因这是电源供给的5V,电流比较大,所以可以使LED发光较亮。拿我们所熟悉的充电宝来说,虽然它的接口输出电压也是5V,但是它流出的电流
[单片机]
51<font color='red'>单片机</font>-蜂鸣器原理
单片机+直流电机转速控制程序+Proteus仿真电路
·1.本设计采用STC89C51/52(与AT89S51/52、AT89C51/52通用,可任选)单片机作为主控制器 ·2.采用霍尔传感器非接触式测电机转速 ·3.LCD1602液晶显示当前的转速,转速单位为转/分(RPM)。和显示当前的pwm占空比0~100%。 ·4.电机的速度可以通过按键调整,也可以开始暂停,正转和反转。 注意:磁铁和霍尔元件最近距离在2mm左右,太近可能会在电机转动时碰到霍尔元件,太远霍尔元件可能会检测不到磁铁。 使用说明: 液晶屏第一行显示电机转速,第二行显示占空比,占空比数值越大,电机转速越快。 系统一共有6个按键,单片机附近的独立按键是系统的复位按键,按下单片机会复位。 下面一排是控制按键
[单片机]
<font color='red'>单片机</font>+直流电机转速控制程序+Proteus仿真电路
AVR单片机入门系列(25)AVR PWM OC0
系统功能 AVR内部脉宽调制OC0输出实验,用LED指示PWM的频率,在PWM速度较低时,可观察到LED的闪烁。 硬件设计 AVR主控电路原理图 软件设计 //目标系统: 基于AVR单片机 //应用软件: ICC AVR /*01010101010101010101010101010101010101010101010101010101010101010101 ---------------------------------------------------------------------- 实验内容: AVR内部脉宽调制输出实验,用LED指示PWM的频率,在PWM速度较低时,可观察到LED的闪烁
[单片机]
<font color='red'>AVR</font><font color='red'>单片机</font>入门系列(25)<font color='red'>AVR</font> PWM OC0
51单片机串口T1加看门狗程序
数码管部分的电路图 程序: #include reg52.h #define UCHAR unsigned char #define UINT unsigned int sfr WDT_CONTR = 0xe1; UCHAR table = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71}; UCHAR timer; UCHAR conter; void initserial(void) { timer='0'; conter=0; TMOD=0X20; SCON=0X50; TL
[单片机]
51<font color='red'>单片机</font>串口T1加看门狗程序
基于单片机C8051F020的数字多电机控制平台设计
   引言   步进电动机因具有转子惯量低、定位精度高、无累积误差等特点,非常适合用于开环位置控制系统中。直流电机是伺服控制中常用的电机。然而在实际系统中为满足不同的功能往往同时存在多个运动部件,常用的方法是一个独立的功能对应一个控制系统,这样虽然模块性很好,但是占用了大量的系统资源和空间,也在一定程度上降低了系统的可靠性。   如在某系统中存在4 个运动部件,分别为两台三相反应式步进电动机,一台直流电机和一台四相步进电动机的控制。本着提高系统集成度的想法,本文只用一个控制芯片C8051F020 就完成了以上4 台电机的驱动控制,电路简单,可靠性高。    1 总体设计   基于Cygnal 公司的MCU 控制芯片C805
[单片机]
基于<font color='red'>单片机</font>C8051F020的数字多电机控制平台设计
单片机程序下载方式ISP、IAP
一般只能通过三种方式下载程序到单片机中:1.JTAG 2.ISP 3.IAP 1.JTAG 要使用JTAG方式下载程序,不管是使用J-LINK、ULINK、ST-LINK,只需要把单片机上相应的程序下载留出来,然后和编程器连接上就可以下载程序了。 2.ISP 要通过ISP方式下载程序,需要用到单片机内部自带的Bootloader,这个Bootloader是预制在单片机内部的,出厂自带的,它在出厂后就不能修改或擦除。因此首先要将BOOT1=0 BOOT0=1,让单片机从系统存储器启动,然后使用ISP下载软件就可以下载程序了。STM32使用的ISP下载软件是mcuisp。ISP可以有很多种方式,比如串口、USB、CAN。
[单片机]
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
随便看看

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved