草庐IT

GPIO模拟UART串口发送和接收

坚持学习的小王同学 2023-08-30 原文

1. 串口通讯协议

通用异步收发器(Universal Asynchronous Receiver/Transmitter),通常称作UART,是一种串行、异步、全双工的通信协议,在嵌入式领域应用的非常广泛。
数据通讯格式:

空闲位:
  UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平,表示当前线路上没有数据传输。

起始位:
  每开始一次通信时发送方先发出一个逻辑”0”的信号(低电平),表示传输字符的开始。因为总线空闲时为高电平所以开始一次通信时先发送一个明显区别于空闲状态的信号即低电平。

数据位:
  起始位之后就是我们所要传输的数据,数据位可以是5、6、7、8,9位等,构成一个字符(一般都是8位)。如ASCII码(7位),扩展BCD码(8位)。先发送最低位,最后发送最高位,使用低电平表示‘0’高电平表示‘1’完成数据位的传输。

奇偶校验位:
  数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验),以此来校验数据传送的正确性。校验位其实是调整个数,串口校验分几种方式:
1、无校验(no parity)。
2、奇校验(odd parity):如果数据位中“1”的数目是偶数,则校验位为“1”,如果“1”的数目是奇数,校验位为“0”。
3、偶校验(even parity):如果数据为中“1”的数目是偶数,则校验位为“0”,如果为奇数,校验位为“1”。
4、mark parity:校验位始终为1(不常用)。
5、parity:校验位始终为0(不常用)。

停止位:
  它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。 由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备之间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟的机会。停止位个数越多,数据传输越稳定,但是数据传输速度也越慢。

波特率:
数据传输速率使用波特率来表示。单位bps(bits per second),常见的波特率9600bps、115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。举个例子,如果串口波特率设置为9600bps,那么传输一个比特需要的时间是1/9600≈104.2us。

2. GPIO模拟UART(9600bps)

2.1. GPIO模拟发送
a. 阻塞式发送:
利用delay()函数进行数据模拟发送(delay 104us):
优点:程序编写简单,适合单纯只有数据发送的情况
缺点:阻塞式发送,CPU占用高,发送时候无法进行其他操作

void gpio_uart_send(unsigned char *data, unsigned char data_len)
{
     unsigned char i = 0, j = 0;
     unsigned char start_flag = 0;
     for (i; i < data_len; i++)
     {
         if (0 == start_flag)
         {
             PORTA &= ~(0x01 << gpio_cfg_data.tx_pin);
             sys_delay_us(gpio_uart_delay_time);
             start_flag = 1;
         }

         for (j = 0; j < gpio_cfg_data.bits; j++)
         {
             if ((data[i] >> j) & 0x01)
             {
                 PORTA |= (0x01 << gpio_cfg_data.tx_pin);
            }
            else
            {
                PORTA &= ~(0x01 << gpio_cfg_data.tx_pin);
            }
             sys_delay_us(gpio_uart_delay_time);
         }

        PORTA |= (0x01 << gpio_cfg_data.tx_pin);
        sys_delay_us(gpio_uart_delay_time);

         start_flag = 0;
     }
}

b. 使用硬件定时器方式发送(推荐使用该方式)
优点:发送过程中不阻塞,可以执行其它任务
缺点:占用一个us级别硬件定时器资源,程序编写有一定逻辑

  1. 初始化一个104us的硬件定时器(以下使用一个51内核单片机为例)
static void __timer0_init(void)
{
    TMR0 = 52; // 时间计算 1/16000000 * 2 * (255-52) * 4 ≈ 104us(16M主频,2T指令周期,255溢出时间,时钟4分频)
    T0IF = 0; //清空T0软件中断
    // T0IE = 1; //开定时器/计数器0中断 
}
  1. 配置GPIO为上拉输出
    // 将TX引脚配置成输出模式,并上拉
    PORTA |= (1 << cfg->tx_pin);
    TRISA &= (1 << cfg->tx_pin);
    WPUA |= (1 << cfg->tx_pin);
  1. 定时器中断中去发送数据
void gpio_uart_send_bit(void)
{
    static unsigned char data_len = 0, bit_cnt = 0;
    static unsigned char bit_start_flag = 1;
    if (1 == bit_start_flag) {
        PORTA &= ~(0x01 << gpio_cfg_data.tx_pin);
        bit_start_flag = 0;
    } else if (bit_cnt < 8) {
        if ((send_data.data[data_len] >> bit_cnt) & 0x01) {
            PORTA |= (0x01 << gpio_cfg_data.tx_pin);
        } else {
            PORTA &= ~(0x01 << gpio_cfg_data.tx_pin);
        }
        bit_cnt++;
    } else {
        bit_start_flag = 1;
        bit_cnt = 0;
        PORTA |= (0x01 << gpio_cfg_data.tx_pin);
        data_len++;
    }
    if (data_len == send_data.data_len) {
        T0IE = 0; //发送完毕,关闭定时器T0
        data_len = 0;
        send_data_cache.finish_flag = 1;
    }
}
  1. 发送api函数
void gpio_uart_send(unsigned char *data, unsigned char data_len)
{
    if (data_len > 5) {
        data_len = 5;
    }

    if (1 == send_data.finish_flag) {
        memcpy(&send_data.data[0], data, data_len);
        send_data.data_len = data_len;
        send_data.finish_flag = 0;
    } else { //缓存一个数据
        memcpy(&send_data_cache.data[0], data, data_len);
        send_data_cache.data_len = data_len;
        send_data_cache.finish_flag = 0;
    }

    if (0 == T0IE) { // 当前未发送
        __timer0_init();
        T0IE = 1; //开定时器/计数器0中断
    }
}

2.2. GPIO模拟接收
GPIO模拟接收也需要一个us级的硬件定时器以及一个GPIO外部中断

  1. 配置接口GPIO为上拉输入,并且开启GPIO中断
    // 将RX引脚配置成输入模式,并上拉
    PORTA &= (1 << cfg->rx_pin);
    TRISA |= (1 << cfg->rx_pin);
    WPUA |= (1 << cfg->rx_pin);

    // 开启PA2中断
    INTEDG = 0; //使能PA2下降沿中断
    INTF = 0;   // 清PA2中断标志位
    INTE = 1;   // 使能PA2中断
  1. 配置定时器52us进入一次中断
static void __timer2_init(void) 
{
	//T2CON = 0; //Bit[1,0]=00,T2时钟分频 1:1
			//Bit[6-3]=0000,T2输出时钟分频1:1 
	T2CON = 0B00000001; //Bit[1,0]=01,T2时钟分频 1:4
			//Bit[6-3]=0000,T2输出时钟分频1:1 
	//T2CON = 0X79; //Bit[1,0]=01,T2时钟分频 1:4
			//Bit[6-3]=1111,T2输出时钟分频1:1 6
	TMR2 = 0;  //TMR2赋初值
	PR2 = 104; //设置TMR2输出比较值定时52us=(1/16000000)*2*4*104(PR2)
											//16M-2T-4分频 

	TMR2IF = 0;//清TMER2中断标志
	TMR2IE = 1;//使能TMER2的中断
	TMR2ON = 1;//使能TMER2启动
	PEIE = 1;    //使能外设中断
}
  1. GPIO中断处理:在接收到下降沿中断来临的时候,开启定时器中断
    if (INTE && INTF) { // RA2 INT中断
        INTF = 0; 
        RA6 = RA2;
        if (RA2) {  //RA2为高电平时,开启下降沿中断
            INTEDG = 0; 
        } else {    //RA2为低电平时,开启上升沿中断
            INTEDG = 1;
            cnt= 0;
            TMR2 = 0; 
            PR2 = 100;
            INTE = 0;//关闭IO中断
        }
    }
  1. 定时器中断处理
//中断函数里面
    if (TMR2IE && TMR2IF) { //定时器52us中断
        TMR2IF = 0;
        TMR2 = 0;
        PR2 = 200; //设置TMR2输出比较值定时104us=(1/16000000)*2*4*200(PR2)
        if (cnt < 0xff) {
            cnt++;
        }
        if ((cnt < 10) && (cnt >= 2)){
            gpio_uart_get_bit(cnt-2, RA2);
        } else if (10 == cnt) {
            INTE = 1;
        } else if (cnt >= 40) {
            cnt = 0xff;
            RA6 = 0;
        }
    }
//中断函数结束
void gpio_uart_get_bit(unsigned char data_bit_cnt, unsigned char data_bit)
{
    unsigned char temp_bit = data_bit;
    if (0 == recive_data.finish_flag) {
        recive_data.data[recive_data.data_len] |= (temp_bit << data_bit_cnt);
    }
    if (7 == data_bit_cnt) {
        recive_data.data_len++;
    }
    if (recive_data.data_len == 5) {
        recive_data.finish_flag = 1;
    }
}

有关GPIO模拟UART串口发送和接收的更多相关文章

  1. ruby - 如何模拟 Net::HTTP::Post? - 2

    是的,我知道最好使用webmock,但我想知道如何在RSpec中模拟此方法:defmethod_to_testurl=URI.parseurireq=Net::HTTP::Post.newurl.pathres=Net::HTTP.start(url.host,url.port)do|http|http.requestreq,foo:1endresend这是RSpec:let(:uri){'http://example.com'}specify'HTTPcall'dohttp=mock:httpNet::HTTP.stub!(:start).and_yieldhttphttp.shou

  2. ruby-on-rails - RSpec:避免使用允许接收的任何实例 - 2

    我正在处理旧代码的一部分。beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)endRubocop错误如下:Avoidstubbingusing'allow_any_instance_of'我读到了RuboCop::RSpec:AnyInstance我试着像下面那样改变它。由此beforedoallow_any_instance_of(SportRateManager).toreceive(:create).and_return(true)end对此:let(:sport_

  3. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

  4. ruby-on-rails - 如何使用 Rack 接收 JSON 对象 - 2

    我有一个非常简单的RubyRack服务器,例如:app=Proc.newdo|env|req=Rack::Request.new(env).paramspreq.inspect[200,{'Content-Type'=>'text/plain'},['Somebody']]endRack::Handler::Thin.run(app,:Port=>4001,:threaded=>true)每当我使用JSON对象向服务器发送POSTHTTP请求时:{"session":{"accountId":String,"callId":String,"from":Object,"headers":

  5. ruby - 使用 Ruby 通过 Outlook 发送消息的最简单方法是什么? - 2

    我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=

  6. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

  7. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  8. ruby - 是否可以在不实际发送或读取数据的情况下查明 ruby​​ 套接字是否处于 ESTABLISHED 或 CLOSE_WAIT 状态? - 2

    s=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)s.connect(Socket.pack_sockaddr_in('port','hostname'))ssl=OpenSSL::SSL::SSLSocket.new(s,sslcert)ssl.connect从这里开始,如果ssl连接和底层套接字仍然是ESTABLISHED,或者它是否在默认值7200之后进入CLOSE_WAIT,我想检查一个线程几秒钟甚至更糟的是在实际上不需要.write()或.read()的情况下关闭。是用select()、IO.select()还是其他方法完成

  9. ruby-on-rails - 在这种情况下我如何模拟一个对象?没有明显的方法可以用模拟替换对象 - 2

    假设我在Store的模型中有这个非常简单的方法:defgeocode_addressloc=Store.geocode(address)self.lat=loc.latself.lng=loc.lngend如果我想编写一些不受地理编码服务影响的测试脚本,这些脚本可能已关闭、有限制或取决于我的互联网连接,我该如何模拟地理编码服务?如果我可以将地理编码对象传递到该方法中,那将很容易,但我不知道在这种情况下该怎么做。谢谢!特里斯坦 最佳答案 使用内置模拟和stub的rspecs,你可以做这样的事情:setupdo@subject=MyCl

  10. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

随机推荐