ATtiny88初体验(三):串口
ATtiny88初体验(三):串口
ATtiny88单片机不包含串口模块,因此只能使用软件方式模拟串口时序。

串口通信时序通常由起始位、数据位、校验位和停止位四个部分组成,常见的配置为1位起始位、8位数据位、无校验位和1位停止位。
模拟串口发送时序
- 设置TX引脚为输出模式,初始电平状态为高电平。
 - 设置定时器周期,以9600波特率为例,将定时器周期设为 \(\frac{1s}{9600} \approx 104us\) 。
 - TX引脚输出低电平(起始位),同时开启定时器。
 - 之后的8次定时器中断,每次输出1位数据,从低位开始。
 - 第9次定时器中断,TX引脚输出高电平(停止位)。
 - 第10次定时器中断,关闭定时器。
 
模拟串口接收时序
- 设置RX引脚为输入模式,使能上拉电阻,开启下降沿中断。
 - 当接收到起始位时,触发下降沿中断,设置定时器周期为 \(\frac{1s}{9600} \times \frac{1}{6} \approx 17us\) ,开启定时器。
 - 之后的30次定时器中断,对RX引脚的电平状态进行计数(起始位)。
 - 第1次定时器中断,将定时器周期重设为 \(\frac{1s}{9600} \times \frac{1}{3} \approx 35us\) 。
 - 第3次定时器中断,如果高电平数量大于低电平数量,则表示起始位接收失败,直接关闭定时器,并开启下降沿中断。
 - 第6/9/.../24/27次定时器中断,判断高电平和低电平的数量,选取数量多的那个电平作为数据位,从低位开始填充。
 - 第30次定时器中断,关闭定时器中断,开启下降沿中断,如果高电平数量大于低电平数量,则表示成功接收到停止位,数据有效。
 
外部中断
ATtiny88有8个外部中断源:INT0、INT1、PCI0、PCI1、PCI2、PCI3。其中INT0/1支持低电平/下降沿/上升沿触发,PCI0/1/2/3在引脚状态改变时触发。
ATtiny88外部中断和引脚的对应关系如下:
| 中断源 | 引脚 | 
|---|---|
| INT0 | PD2 | 
| INT1 | PD3 | 
| PCI0 | PB[0:7] -> PCINT[0:7] | 
| PCI1 | PC[0:7] -> PCINT[8:15] | 
| PCI2 | PD[0:7] -> PCINT[16:23] | 
| PCI3 | PA[0:3] -> PCINT[24:27] | 
注意:即使引脚配置为输出模式,也能触发相应的中断。
寄存器

ISC1[1:0]:设置INT1中断触发方式。

ISC0[1:0]:设置INT0中断触发方式,取值同ISC1[1:0]。

INT1:设为1使能INT1中断。INT0:设为1使能INT0中断。

INTF1:INT1中断标志位,执行中断函数时自动清零,也可以写1清零。INTF0:INT0中断标志位,执行中断函数时自动清零,也可以写1清零。

PCIE3:设为1使能PCI3(PCINT[27:24])中断。PCIE2:设为1使能PCI2(PCINT[23:16])中断。PCIE1:设为1使能PCI1(PCINT[15:8])中断。PCIE0:设为1使能PCI0(PCINT[7:0])中断。

PCIF3:PCI3(PCINT[27:24])中断标志位,执行中断函数时自动清零,也可以写1清零。PCIF2:PCI2(PCINT[23:16])中断标志位,执行中断函数时自动清零,也可以写1清零。PCIF1:PCI1(PCINT[15:8])中断标志位,执行中断函数时自动清零,也可以写1清零。PCIF0:PCI0(PCINT[7:0])中断标志位,执行中断函数时自动清零,也可以写1清零。




PCINTx:设为1使能PCINTx中断。
代码实现
inc/serial.h 头文件的代码内容如下:
#pragma once
#include <stdint.h>
#define UART    (&serial)
typedef struct {
    const uint8_t *cfg;
    uint8_t flag;
    uint8_t tx_idx;
    uint8_t tx_temp;
    uint8_t tx_data;
    uint8_t rx_idx;
    uint8_t rx_temp;
    uint8_t rx_data;
    uint8_t rx_cnt;
} serial_t;
typedef enum {
    SERIAL_BR_1200 = 0,
    SERIAL_BR_2400,
    SERIAL_BR_4800,
    SERIAL_BR_9600,
    SERIAL_BR_19200,
    SERIAL_BR_38400,
    SERIAL_BR_57600,
    SERIAL_BR_115200
} serial_baudrate_t;
typedef enum {
    SERIAL_FLAG_TXE = 0x01,
    SERIAL_FLAG_RXNE = 0x02
} serial_flag_t;
extern serial_t serial;
void serial_setup(serial_t *serial, serial_baudrate_t br);
uint8_t serial_get_flag(serial_t *serial, serial_flag_t flag);
void serial_send_data(serial_t *serial, uint8_t data);
uint8_t serial_receive_data(serial_t *serial);
src/serial.c 源文件的代码内容如下,其中将PD1引脚定义为TX,将PD2引脚定义为RX:
#include <serial.h>
#include <avr/io.h>
#include <avr/interrupt.h>
serial_t serial;
static const uint8_t serial_cfg[] = {
    0x03, 208, 35, 69,  // 1200
    0x03, 104, 17, 35,  // 2400
    0x03, 52, 9, 17,    // 4800
    0x02, 208, 35, 69,  // 9600
    0x02, 104, 17, 35,  // 19200
    0x02, 52, 9, 17,    // 38400
    0x02, 35, 6, 12,    // 57600
    0x01, 139, 23, 46,  // 115200
};
void serial_setup(serial_t *serial, serial_baudrate_t br)
{
    serial->cfg = &serial_cfg[br * 4];
    serial->flag = SERIAL_FLAG_TXE;         // initial value for serial->flag
    // setup tx pin
    PORTD |= _BV(PORTD1);                   // PD1 outputs high level
    DDRD |= _BV(DDD1);                      // set PD1 as output
    // setup rx pin
    PORTD |= _BV(PORTD2);                   // enable PD2 pull-up resistance
    DDRD &= ~_BV(DDD2);                     // set PD2 as input
    // setup INT0
    EICRA &= ~(_BV(ISC01) | _BV(ISC00));
    EICRA |= _BV(ISC01);                    // the falling edge of INT0 generates an interrupt request
    EIFR = _BV(INTF0);                      // clear INT0 interrupt flag
    EIMSK |= _BV(INT0);                     // enable INT0 interrupt
    // setup TIMER0
    TCNT0 = 0;                              // clear counter
    TIMSK0 = 0;                             // disable all interrupts of TIMER0
    TIFR0 = _BV(OCF0B) | _BV(OCF0A);        // clear TIMER0_COMPA & TIMER0_COMPB interrupt flags
    TCCR0A = serial->cfg[0];                // set mode & prescaler of TIMER0
}
uint8_t serial_get_flag(serial_t *serial, serial_flag_t flag)
{
    return serial->flag & flag;
}
void serial_send_data(serial_t *serial, uint8_t data)
{
    serial->flag &= ~SERIAL_FLAG_TXE;       // clear TXE flag
    serial->tx_data = data;                 // store the data to transmit
    serial->tx_temp = data;
    serial->tx_idx = 0;                     // reset index of transmission
    OCR0A = TCNT0 + serial->cfg[1] - 1;     // set period of TIMER0_COMPA
    PORTD &= ~_BV(PORTD1);                  // PD1 outputs low level
    TIFR0 = _BV(OCF0A);                     // clear TIMER0_COMPA interrupt flag
    TIMSK0 |= _BV(OCIE0A);                  // enable TIMER0_COMPA interrupt
}
uint8_t serial_receive_data(serial_t *serial)
{
    uint8_t data = serial->rx_data;         // read the data received
    serial->flag &= ~SERIAL_FLAG_RXNE;      // clear RXNE flag
    return data;
}
static inline void serial_tx_timer_isr(serial_t *serial)
{
    if (serial->tx_idx < 8) {               // send databits
        if (serial->tx_temp & 0x01) {       // output the lowest bit
            PORTD |= _BV(PORTD1);
        } else {
            PORTD &= ~_BV(PORTD1);
        }
        serial->tx_temp >>= 1;
    } else if (serial->tx_idx == 8) {       // send stopbit
        PORTD |= _BV(PORTD1);
    } else {                                // end of transmission
        serial->flag |= SERIAL_FLAG_TXE;    // set TXE flag
        TIMSK0 &= ~_BV(OCIE0A);             // disable TIMER0_COMPA interrupt
    }
    OCR0A += serial->cfg[1];                // set time of the next interrupt
    serial->tx_idx++;                       // update index of transmission
}
static inline void serial_rx_int_isr(serial_t *serial)
{
    OCR0B = TCNT0 + serial->cfg[2] - 1;     // set time of the first TIMER0_COMPB interrupt
    EIMSK &= ~_BV(INT0);                    // disable INT0 interrupt
    TIFR0 = _BV(OCF0B);                     // clear TIMER0_COMPB interrupt flag
    TIMSK0 |= _BV(OCIE0B);                  // enable TIMER0_COMPB interrupt
    serial->rx_idx = 0;                     // reset index of reception
    serial->rx_cnt = 0;                     // clear counter of 0/1
}
static inline void serial_rx_timer_isr(serial_t *serial)
{
    serial->rx_cnt += PIND & _BV(PIND2) ? 0x10 : 0x01;  // count 0/1
    if (serial->rx_idx == 2) {              // receive startbit
        if (serial->rx_cnt > 0x20) {        // if startbit is '1'
            TIMSK0 &= ~_BV(OCIE0B);         // disable TIMER0_COMPB interrupt
            EIFR = _BV(INTF0);              // clear INT0 interrupt flag
            EIMSK |= _BV(INT0);             // enable INT0 interrupt flag
        }
        serial->rx_cnt = 0;                 // reset counter of 0/1
    } else if (serial->rx_idx == 29) {      // receive stopbit
        if (serial->rx_cnt > 0x20) {        // if stopbit is '1'
            serial->rx_data = serial->rx_temp;  // the data received is valid, store it to serial->rx_data
            serial->flag |= SERIAL_FLAG_RXNE;   // set RXNE flag
        }
        TIMSK0 &= ~_BV(OCIE0B);             // disable TIMER0_COMPB interrupt
        EIFR = _BV(INTF0);                  // clear INT0 interrupt flag
        EIMSK |= _BV(INT0);                 // clear INT0 interrupt flag
    } else if (serial->rx_idx % 3 == 2) {   // receive databits
        serial->rx_temp >>= 1;
        if (serial->rx_cnt > 0x20) {
            serial->rx_temp |= 0x80;
        }
        serial->rx_cnt = 0;                 // reset counter of 0/1
    }
    OCR0B += serial->cfg[3];                // set time of the next interrupt
    serial->rx_idx++;                       // update index of reception
}
ISR(TIMER0_COMPA_vect)
{
    uint8_t sreg = SREG;
    serial_tx_timer_isr(UART);
    SREG = sreg;
}
ISR(INT0_vect)
{
    uint8_t sreg = SREG;
    serial_rx_int_isr(UART);
    SREG = sreg;
}
ISR(TIMER0_COMPB_vect)
{
    uint8_t sreg = SREG;
    serial_rx_timer_isr(UART);
    SREG = sreg;
}
注意:实测115200以下(含)的波特率发送都正常,但是9600以上(不含)的波特率接收不正常,建议日常使用9600波特率。
重定向stdio到串口
为了更方便的使用串口,可以将标准输入输出重定向到串口,在AVR GCC中的做法如下:
定义输入和输出的接口函数,原型如下:
int putc(char c, FILE *stream);
int getc(FILE *stream);
使用
FDEV_SETUP_STREAM创建一个stream。FILE s = FDEV_SETUP_STREAM(putc, getc, flag)
将上面创建的stream替换掉
stdout/stdin。stdout = stdin = &s;
代码实现
src/main.c 源文件的代码内容如下:
#include <stdint.h>
#include <stdio.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <serial.h>
static void stdio_setup(void);
int main(void)
{
    cli();
    stdio_setup();
    sei();
    printf("Hello, ATtiny88!\r\n");
    for (;;) {
        putchar(getchar());
    }
}
static int serial_putchar(char c, FILE *stream)
{
    while (!serial_get_flag(UART, SERIAL_FLAG_TXE));
    serial_send_data(UART, c);
    return 0;
}
static int serial_getchar(FILE *stream)
{
    while (!serial_get_flag(UART, SERIAL_FLAG_RXNE));
    return serial_receive_data(UART);
}
static void stdio_setup(void)
{
    static FILE f = FDEV_SETUP_STREAM(serial_putchar, serial_getchar, _FDEV_SETUP_RW);
    serial_setup(UART, SERIAL_BR_9600);
    stdout = &f;
    stdin = &f;
}
参考资料
ATtiny88初体验(三):串口的更多相关文章
- Ruby on rails初体验(三)
		
继体验一和体验二中的内容,此节将体验二中最开始的目标来实现,体验二中已经将部门添加的部分添加到了公司的show页面,剩下的部分是将部门列表也添加到公司的显示页面,整体思路和体验二中相同,但是还是会有点 ...
 - Spring Cloud Alibaba 初体验(三) Nacos 与 Dubbo 集成
		
一.新建项目 新建项目,只放置接口,用于暴露 Dubbo 服务接口 public interface GreetingService { String greeting(); } 二.provider ...
 - CentOS 初体验三: Yum 安装、卸载软件
		
一:Yum 简介 Yum(全称为 Yellow dog Updater, Modified)是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器.基于RPM包管理,能够从指 ...
 - $.extend({},defaults, options) --(初体验三)
		
1.$.extend({},defaults, options) 这样做的目的是为了保护包默认参数.也就是defaults里面的参数. 做法是将一个新的空对象({})做为$.extend的第一个参数, ...
 - Swift初体验(三)
		
/*******************************************************************************/ // 协议 protocol Des ...
 - JSON初体验(三):FastJson解析
		
JSON解析之FastJson(阿里巴巴解析开源) 特点: Fastjson是一个Java语言编写的高性能功能完善的JSON库,它采用的 是一种"假定有序快速匹配"的算法,把JSO ...
 - 第三次随笔--安装虚拟机及学习linux系统初体验
		
第三次随笔--安装虚拟机及学习linux系统初体验 ·学习基于VirtualBox虚拟机安装Ubuntu图文教程在自己笔记本上安装Linux操作系统 首先按照老师的提示步骤进行VirtualBox虚拟 ...
 - 20155315庄艺霖第三次作业之Linux初体验
		
Linux初体验 安装Linux三两事 老师的作业要求基于VirtualBox安装Linux系统,我一开始下载了VB但是电脑运行不了,后来看网上的教程下载了VMware,才算开始了我的Linux之旅. ...
 - 深入Asyncio(三)Asyncio初体验
		
Asyncio初体验 Asyncio在Python中提供的API很复杂,其旨在替不同群体的人解决不同的问题,也正是由于这个原因,所以很难区分重点. 可以根据asyncio在Python中的特性,将其划 ...
 - Linux内核驱动学习(三)字符型设备驱动之初体验
		
Linux字符型设备驱动之初体验 文章目录 Linux字符型设备驱动之初体验 前言 框架 字符型设备 程序实现 cdev kobj owner file_operations dev_t 设备注册过程 ...
 
随机推荐
- 2023-05-21:给定一个字符串 s 和一个整数 k 。你可以从 s 的前 k 个字母中选择一个, 并把它加到字符串的末尾。 返回 在应用上述步骤的任意数量的移动后,字典上最小的字符串。 输入:s
			
2023-05-21:给定一个字符串 s 和一个整数 k .你可以从 s 的前 k 个字母中选择一个, 并把它加到字符串的末尾. 返回 在应用上述步骤的任意数量的移动后,字典上最小的字符串. 输入:s ...
 - remote: HTTP Basic:Access denied fatal:Authentication failed for
			
近来在一天新电脑上面使用git pull 一个项目,老是提示 Access denied, 找了许多方法,ssh key这些都配置了还是不行,当时别提有多尬 看嘛这就是pull 时的提示 // *** ...
 - Windows常用的 CMD 命令合集
			
常用的 CMD 命令合集: 基础命令 dir:列出当前目录中的文件和子目录. cd:更改当前目录.例如,cd Documents 将当前目录更改为 Documents 文件夹. md 或 mkdir: ...
 - rest framework 学习 序列化
			
序列化功能:对请求数据进行验证和对Queryset进行序列化 Queryset进行序列化: 1 序列化之Serializer 1 class UserInfoSerializ ...
 - 基于 prefetch 的 H5 离线包方案
			
前言 对于电商APP来讲,使用H5技术开发的页面占比很高.由于H5加载速度非常依赖网络环境,所以为了提高用户体验,针对H5加载速度的优化非常重要.离线包是最常用的优化技术,通过提前下载H5渲染需要的H ...
 - 【翻译】高效numpy指北
			
ref:link why numpy 运算高效 numpy 内存结构 一块内存区域 dtype 确定了内存区域数据类型 metadata 比如 shape.strides etc 注:numpy 内存 ...
 - 03-面试必会-Mysql篇
			
1. Mysql 查询语句的书写顺序 Select [distinct ] <字段名称> from 表 1 [ <join 类型> join 表 2 on <join 条 ...
 - IOS开发--UILabel的基本使用
			
UILabel是iOS中用于显示静态文本的控件. 它的主要功能是:1. 显示一行或多行文本 UILabel可以用来显示单行或多行文本内容.通过设置numberOfLines属性可以控制文本显示的行数. ...
 - VisionPro学习笔记(2)——图像转换工具ImageCovertTool
			
众所周知,VisionPro是一款功能强大的机器视觉软件,用于开发和部署机器视觉应用程序.其中ImageConvertTool是其中一个重要的工具,用于图像转换和处理.本文将介绍如何使用ImageCo ...
 - WPF复习知识点记录
			
WPF复习知识点记录 由于近几年主要在做Web项目,客户端的项目主要是以维护为主,感觉对于基础知识的掌握没有那么牢靠,趁着这个周末重新复习下WPF的相关知识. 文章内容主要来自大佬刘铁锰老师的经典著作 ...