ATtiny88初体验(三):串口

ATtiny88单片机不包含串口模块,因此只能使用软件方式模拟串口时序。

串口通信时序通常由起始位、数据位、校验位和停止位四个部分组成,常见的配置为1位起始位、8位数据位、无校验位和1位停止位。

模拟串口发送时序

  1. 设置TX引脚为输出模式,初始电平状态为高电平。
  2. 设置定时器周期,以9600波特率为例,将定时器周期设为 \(\frac{1s}{9600} \approx 104us\) 。
  3. TX引脚输出低电平(起始位),同时开启定时器。
  4. 之后的8次定时器中断,每次输出1位数据,从低位开始。
  5. 第9次定时器中断,TX引脚输出高电平(停止位)。
  6. 第10次定时器中断,关闭定时器。

模拟串口接收时序

  1. 设置RX引脚为输入模式,使能上拉电阻,开启下降沿中断。
  2. 当接收到起始位时,触发下降沿中断,设置定时器周期为 \(\frac{1s}{9600} \times \frac{1}{6} \approx 17us\) ,开启定时器。
  3. 之后的30次定时器中断,对RX引脚的电平状态进行计数(起始位)。
  4. 第1次定时器中断,将定时器周期重设为 \(\frac{1s}{9600} \times \frac{1}{3} \approx 35us\) 。
  5. 第3次定时器中断,如果高电平数量大于低电平数量,则表示起始位接收失败,直接关闭定时器,并开启下降沿中断。
  6. 第6/9/.../24/27次定时器中断,判断高电平和低电平的数量,选取数量多的那个电平作为数据位,从低位开始填充。
  7. 第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中的做法如下:

  1. 定义输入和输出的接口函数,原型如下:

    int putc(char c, FILE *stream);
    int getc(FILE *stream);
  2. 使用 FDEV_SETUP_STREAM 创建一个stream。

    FILE s = FDEV_SETUP_STREAM(putc, getc, flag)
  3. 将上面创建的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;
}

参考资料

  1. avr-libc: <stdio.h>: Standard IO facilities
  2. ATtiny88 Datasheet

ATtiny88初体验(三):串口的更多相关文章

  1. Ruby on rails初体验(三)

    继体验一和体验二中的内容,此节将体验二中最开始的目标来实现,体验二中已经将部门添加的部分添加到了公司的show页面,剩下的部分是将部门列表也添加到公司的显示页面,整体思路和体验二中相同,但是还是会有点 ...

  2. Spring Cloud Alibaba 初体验(三) Nacos 与 Dubbo 集成

    一.新建项目 新建项目,只放置接口,用于暴露 Dubbo 服务接口 public interface GreetingService { String greeting(); } 二.provider ...

  3. CentOS 初体验三: Yum 安装、卸载软件

    一:Yum 简介 Yum(全称为 Yellow dog Updater, Modified)是一个在Fedora和RedHat以及CentOS中的Shell前端软件包管理器.基于RPM包管理,能够从指 ...

  4. $.extend({},defaults, options) --(初体验三)

    1.$.extend({},defaults, options) 这样做的目的是为了保护包默认参数.也就是defaults里面的参数. 做法是将一个新的空对象({})做为$.extend的第一个参数, ...

  5. Swift初体验(三)

    /*******************************************************************************/ // 协议 protocol Des ...

  6. JSON初体验(三):FastJson解析

    JSON解析之FastJson(阿里巴巴解析开源) 特点: Fastjson是一个Java语言编写的高性能功能完善的JSON库,它采用的 是一种"假定有序快速匹配"的算法,把JSO ...

  7. 第三次随笔--安装虚拟机及学习linux系统初体验

    第三次随笔--安装虚拟机及学习linux系统初体验 ·学习基于VirtualBox虚拟机安装Ubuntu图文教程在自己笔记本上安装Linux操作系统 首先按照老师的提示步骤进行VirtualBox虚拟 ...

  8. 20155315庄艺霖第三次作业之Linux初体验

    Linux初体验 安装Linux三两事 老师的作业要求基于VirtualBox安装Linux系统,我一开始下载了VB但是电脑运行不了,后来看网上的教程下载了VMware,才算开始了我的Linux之旅. ...

  9. 深入Asyncio(三)Asyncio初体验

    Asyncio初体验 Asyncio在Python中提供的API很复杂,其旨在替不同群体的人解决不同的问题,也正是由于这个原因,所以很难区分重点. 可以根据asyncio在Python中的特性,将其划 ...

  10. Linux内核驱动学习(三)字符型设备驱动之初体验

    Linux字符型设备驱动之初体验 文章目录 Linux字符型设备驱动之初体验 前言 框架 字符型设备 程序实现 cdev kobj owner file_operations dev_t 设备注册过程 ...

随机推荐

  1. 2021-05-09:给定数组hard和money,长度都为N;hard[i]表示i号的难度, money[i]表示i号工作的收入;给定数组ability,长度都为M,ability[j]表示j号人的

    2021-05-09:给定数组hard和money,长度都为N:hard[i]表示i号的难度, money[i]表示i号工作的收入:给定数组ability,长度都为M,ability[j]表示j号人的 ...

  2. SQL Server 2014 英文版安装教程

    安装过程如下 1. 点击setup开始安装. 2. 选择如下的全新安装. 3. 自动生成产品密钥,然后点击下一步. 4. 勾选接受条款,然后点击下一步. 5. 自动更新根据实际情况进行选择,点击下一步 ...

  3. npm安装报错

    npm ERR! request to https://registry.cnpmjs.org/element-ui failed, reason: Hostname/IP does not matc ...

  4. drf——restful规范、序列化反序列化、drf介绍和快速使用、drf之APIView源码

    1.restful规范 # restful是一种定义API接口的设计风格,API接口的编写规范,尤其适用于前后端分离的应用模式中 这种风格的理念人为后端开发任务就是提供数据的,对外提供的是数据资源的访 ...

  5. 3 分钟利用 FastGPT 和 Laf 将 ChatGPT 接入企业微信

    原文链接:https://forum.laf.run/d/556 FastGPT 是一个超级的 ChatGPT 平台项目,功能非常强大: 集成了 ChatGPT.GPT4 和 Claude 可以使用任 ...

  6. MAC 打开.bash_profile

    1.开启终端(terminal)[左下角启动台(图标)> 其他] 2.进入当前用户目录 $ cd ~ 3.打开profile文件 $ open -e .bash_profile 就会弹出.bas ...

  7. v8 study

    v8环境搭建看这里 现在的v8采用的是Ignition(JIT生成) + TurboFan(优化) v8调试 安装pwngdb git clone https://github.com/pwndbg/ ...

  8. 案例分享-被*队友的mybatis蠢哭的一天

    昨晚加班的时候被队友拉着看一个mybatis的问题,耗费了我一个小时时间,最后差点没被我打死,实在是觉得滑稽,今天回家写下来跟大伙分享一下. 问题现象 Invalid bound statement ...

  9. 【python基础】循环语句-continue关键字

    1.continue关键字 continue关键字的作用是:用来告诉 Python 跳过当前循环代码块中的剩余语句,然后继续进行下一轮循环. 其在while循环和for循环中的作用示意图如下 我们通过 ...

  10. cmake 安装一个目录下的图片 到另一个目录文件中去

    install(DIRECTORY ./cfg/labels/ DESTINATION ./fservo/cfg/yolo_cfg/labels/) install (DIRECTORY ./cfg/ ...