单片机的软件串口通信

串口通信往往是我们学到的第一个最简单的通信方式,也几乎是最广泛的通信方式,在一个设备和另一个设备之间通信,因为流程简单,协议易懂,所以非常常用

不过我们用资源丰富的单片机比如stm32时,都是硬件帮我们完成了整个流程。而在资源有限的8位单片机上,如果没有硬件串口,就需要我们一点点用定时器和电平手搓出来

串口协议

串口的协议非常简单

  • 空闲状态: 高电平(上拉电阻)
  • 起始位: 开始通信时先下拉一个低电平,触发流程
  • 数据位: 发8位0或1的数据
  • 停止位: 最后拉高一个电平就发送结束了。校验位我们一般跳过,下面讲
  • 我们要提前商议好通信速率

串口单位是波特率bps,每秒比特数,就是每秒有几个位。

所以一个电平的时间比如1200bps,就是1秒/1200bps = 833us一个电平周期。

我们一般不用校验位,是因为校验位只能检测,只有在,所以长期习惯就使用8数据位、无校验、1停止位的配置

常用的串口标准是TTL,就是3.3V高电平,0V低电平,但其实它只是一种电压标准 ,用什么样的电压来表示逻辑1和逻辑0。还有很多不同协议也是串口。
RS232是+15V高电平,-15V低电平,电压较高适合远距离(比如20m)环境


RS485为了更加抗干扰,用的是差分信号,两根线B>A是1,A>B是0,这是一种天才的设计,实现了极强的抗干扰屏蔽效果,传输距离可以达1200m!bilibili【5分钟看懂!串口RS232 RS485最本质的区别!】

设备配置

现在我们拿出单片机开始手搓,资源只需要用到一个定时器 和一个外部(电平)中断

其中外部中断 就是实现接收时,起始位拉低一个电平的触发效果。

以1200bps 833us速率为例

  • 发送: 在833的定时器内,先发一个低电平。再发8位的数据。最后发一位低电平

发送函数部分代码

c 复制代码
   	  if (tx_cnt == 0) // 发送起始位
       {
           TXD = 0;
           tx_byte = tx_buf[tx_num];
       }
       else if (tx_cnt == 9) // 发送结束位
       {
           TXD = 1;
       }
       else // 1~8 数据位
       {
           if ((tx_byte & 0x01) == 0x01)
               TXD = 1;
           else
               TXD = 0;
           tx_byte = tx_byte >> 1;		// 移下一位
       }
  • 接收:
    但是接收麻烦一点,我们不知道周期什么时候开始 ,所以不能用一个固有的定时器周期实现,万一我们的定时器周期和发送设备的周期不匹配就连不上。所以,我们可以用4倍频 ,也就是4倍的周期,然后选择一个位置进行接收采样
    先用一个外部中断 ,触发低电平,然后延后半个电平 周期进入定时器开始定时接收,也就是取信号的中间位置 ,提高接收成功率,因为实际信号中,电平跳变时有突刺尖峰,特别是远距离的时候

最终的流程是:

外部中断触发低电平,打开接收标志位 -> 4倍频的定时器延后两个周期后,开始运行接收函数 -> 接收完毕后,重新打开外部中断

发送就是每4个周期发一个信号

定时器代码

在main函数的定时器部分,注意初始化

c 复制代码
#include "soft_uart.h"
//===串口数据=======================================

#define UART_MAX_LEN 20

// 串口发送
unsigned char tx_buf[UART_MAX_LEN] = {0}; // 要发送的数据
unsigned char tx_len = 0;       // 要发送的长度。有数据时发送

// 串口接收
unsigned char rx_buf[UART_MAX_LEN] = {0};    // 接收缓冲区
unsigned char rx_len = 0;      // 接收的字节数

void interrupt Timer_Isr()
{
	static unsigned char flag_rx = 0; // 开始接收
	static unsigned char rx_time_count = 0; // 串口接收计数器,4时一个周期
	static unsigned char tx_time_count = 0;
	
	//外部中断  串口接收起始位
	if((RBIE)&&(RBIF))   //外部下降沿中断
	{
        RBIF = 0;			//清中断标志
		if (RXD == 0) 	//起始位为低电平	
		{	
			// 关闭外部中断
			RBIE = 0;
			
			// 开始串口接收
			flag_rx = 1;
			rx_time_count = 2; // 半个周期中间采样,增加成功率
		}
		else 
		{
            RBIF = 0;	// 清中断标志    
            PORTB;      // 刷新端口状态
		}
	}

	//定时中断0 833us/4 = 208us
	if((T0IF)&&(T0IE))	//833us  1200波特率
	{
        TMR0 += (256 - BUND_PR);
        T0IF = 0;	//清中断标志位


		//串口接收
		if(flag_rx == 1)
		{
			rx_time_count--;
			if(rx_time_count == 0) //串口接收,833us,4个周期
			{	
				rx_time_count = 4;
				
				static unsigned char rx_byte;	// 接收的一个字节
				unsigned char uart_ret = 0;//接收结果
				
				// 串口接收
				uart_ret = rx_buf_polling(&rx_byte);

				// 成功接收一个字节
				if( uart_ret == 1 )
				{
					// 存储到接收缓冲区
					rx_buf[rx_len++] = rx_byte;
					// 超出缓冲区长度,回滚到开始
					if (rx_len >= UART_MAX_LEN)
					{
						rx_len = 0;
					}

					// 关闭接收
					flag_rx = 0;

					// 重新从外部中断开始
					RBIF = 0;
					RBIE = 1; //  打开外部中断
					PORTB;     

				}
				else if( uart_ret == 2 ) // 接收失败
				{
					// 关闭接收
					flag_rx = 0;
					
					// 重新从外部中断开始
					RBIF = 0;
					RBIE = 1; //  打开外部中断
					PORTB;     

				}
			}
		}
        
		// 串口发送
		tx_time_count--;
        if(tx_time_count == 0) //串口发送,833us,4个周期
        {	
			tx_time_count = 4;
            tx_buf_polling(tx_buf,&tx_len); // 有数据时串口自动发送
        }
	}
}

协议代码

移植适配时,只要适配引脚TXD RXD的实现就行。注意RX,TX初始化成输入和输出模式
soft_uart.h

c 复制代码
#ifndef __SOFT_UART_H__
#define __SOFT_UART_H__

#include "sc.h"  // 根据实际需要包含必要的头文件

#define TXD RB1
#define RXD RB0

// 函数声明
// 发送轮询函数
void tx_buf_polling( const unsigned char *tx_buf, unsigned char *tx_len);
// 接收轮询函数
unsigned char rx_buf_polling(unsigned char *rx_byte);  

#endif /* __SOFT_UART_H__ */

soft_uart.c

c 复制代码
/*
     软件接收串口
*/
#include "soft_uart.h"
#include <string.h>


/*-------------------------------------------------
 *  函数名: Send_buf_polling
 *	功能:  发送函数  在定时器里循环
 *  输入:  tx_start-开始标志位,tx_buf-要发送的数据,tx_len-要发送的长度
 --------------------------------------------------*/
void tx_buf_polling( const unsigned char *tx_buf, unsigned char *tx_len)
{
    static unsigned char tx_num = 0;	
	static unsigned char tx_cnt = 0;
	static unsigned char tx_byte = 0; // 当前发送的字节
    
	if (*tx_len > 0) // 发送使能位
    {
        if (tx_num < *tx_len)
        {
            if (tx_cnt == 0) // 发送起始位
            {
                TXD = 0;
                tx_byte = tx_buf[tx_num];
            }
            else if (tx_cnt == 9) // 发送结束位
            {
                TXD = 1;
            }
            else // 1~8 数据位
            {
                if ((tx_byte & 0x01) == 0x01)
                    TXD = 1;
                else
                    TXD = 0;
                tx_byte = tx_byte >> 1;		// 移下一位
            }

            tx_cnt++;
            if(tx_cnt > 9)  // 一个字节发送完
            {
                tx_cnt = 0; 
                tx_num++;  
            }
        }
        else
        {
            //tx_start = 0; 
            tx_num = 0;
            tx_cnt = 0;
			*tx_len = 0; // 结束发送
        }
    }
}

/*-------------------------------------------------
 *  函数名: rx_buf_polling
 *  功能:  接收函数 (在定时器中循环)
 *  输入:  rx_byte-接收到的字节指针
 *  输出:  0-正常,1-接收一个字节,2-出错
--------------------------------------------------*/
unsigned char rx_buf_polling(unsigned char *rx_byte)
{
    static unsigned char rx_bit_cnt = 0;

    if (rx_bit_cnt == 0)
    {
        // 起始位为0 验证成功
        if (RXD == 0)
        {
            rx_bit_cnt = 1;
            *rx_byte = 0;
            return 0;
        }
        else
        {
            // 起始位错误,关闭接收
            rx_bit_cnt = 0;
            *rx_byte = 0;
            return 2;
        }
    }
    else if (rx_bit_cnt >= 1 && rx_bit_cnt <= 8)
    {
        // 接收数据位
        *rx_byte >>= 1;
        if (RXD == 1)
        {
            *rx_byte |= 0x80;
        }
    }
    else if (rx_bit_cnt == 9)
    {
        // 检查停止位
        if (RXD == 1)
        {
            // 接收成功
            rx_bit_cnt = 0;
            return 1; // 成功接收一个字节
        }
        else
        {
            // 停止位错误
            rx_bit_cnt = 0;
            *rx_byte = 0;
            return 2;
        }
    }
    rx_bit_cnt++;
    return 0;
}
相关推荐
d111111111d2 小时前
在STM32中,中断服务函数的命名有什么要求?
笔记·stm32·单片机·嵌入式硬件·学习·c#
易水寒陈2 小时前
MultiTimer源码分析
stm32·单片机
白羽陌3 小时前
STM32入门教程
stm32·单片机·嵌入式硬件
高工智能汽车3 小时前
车规MCU,开启“巨变”
单片机·嵌入式硬件
TEL136997627504 小时前
PTCB818A说明书 配套PL27A1芯片MCU参数说明
网络·单片机·嵌入式硬件
客家元器件4 小时前
LPDDR5选型参数
嵌入式硬件
✎ ﹏梦醒͜ღ҉繁华落℘4 小时前
实际项目开发单片机—Flash错误
单片机
一个平凡而乐于分享的小比特4 小时前
单片机内部时钟 vs 外部时钟详解
单片机·嵌入式硬件·内部时钟·外部时钟
xyx-3v5 小时前
RK3506G移植APM飞控的可行性
单片机·学习