串口通信往往是我们学到的第一个最简单的通信方式,也几乎是最广泛的通信方式,在一个设备和另一个设备之间通信,因为流程简单,协议易懂,所以非常常用
不过我们用资源丰富的单片机比如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;
}