网上有好多关于利用IO口来实现串口数据收发的实例,这种方法的实质都是可以应用于任何一款微处理器上,而不仅仅局限于stm32。有相关的源代码链接参考:
当然,如果成本要求不高,可以利用串口扩展芯片是最方便的,如CH438,WK系列芯片等。
IO口模拟串口的一些基本原理,阐述如下:
STM32单片机一般少则3个串口,多则5个,而项目还偏偏5个硬件串口还是不够用.板子上有几个预留IO口,可以用来模拟串口. 模拟串口一般都选9600,速度最快试了也才19200,所以限制还是较多的,一般不得以情况下才会用到.
IO口模拟串口的思路也比较简单,一切按照串口协议进行操作即可。对于发送,计算好不同波特率对应的延时时间进行数据发送。对于接收,稍微复杂。通过外部中断检测接收管脚的下降沿,检测到起始信号后开启定时器,定时器按照波特率设定好时间,每隔一段时间进入定时器中断接收数据,完成一个字节后关闭定时器。
模拟串口分收和发:收比较难,发送比较容易,那就先将接收这块吧。 接收:
有2种思路: 一种是第一个下降沿开始启动定时器,每个bit都去采样Rx电平; 从第一个边沿开始统计时间戳,接收完10个bit后再解析. 我这边碰巧选择的是第二种方法,具体实现思路是:
① 启动定时器2用来做背景时间,定时器分频后的计数频率为1MHz,那么对于9600bps来说1个bit就是104.16us,我们取整数104就可以了;
② 再同时启动一个定时器3,定时时间为1049.5,这个定时器中断发生的时候表示 一个字节接收完毕了
我们以0x37串口通讯时序图为例, ,传输的时候是LSB 1st, Rx引脚选择边沿触发, 第1次边沿到的时候
启动定时器2,同时启动定时器3, 第2个边沿的时候将定时器2的计数值存到数组里,此时是104,第3个
边沿触发的时候将其计数1045计数值,同理将4,5,6三个边沿对应的TIM2值存到缓存里,最终是 0,104,416,520,728,936,除以104变成0,1,4,5,7,9;
如何将0,1,4,5,7,9解析成0x37,看似简单的问题其实还是有点麻烦的~ 给个思路,先将0和9这样的数字去掉,变成1,4,5,7;然后从1开始数到8,跟数组里元素是否有匹配,如果匹配就将状态取反,没有就维持之前的状态:1有,2,3没有,4有,5有,6没有,7有,8没有–>11101100->倒序后就是0x37了;
再来分析几个处理后的值:4,5,7: 1,2,3没有,4有,5有,6没有,7有,8没有–>00010011->逆序后11001000->0xC8 1,2: 1有,2有,3,4,5,6,7,8没有->10000000->逆序后00000001->0x01
1,2,3,4,6,7,8:1有,2有,3有,4有,5没有,6有,7有,8有->10100101->逆序后10100101->0xA5
7,8:1,2,3,4,5,6没有,7有,8有->00000010->逆序后01000000->0x40
总结下如果第一个是1开头的那么得出的bit就是1,如果第一个是其他数字开头的,则 “1到其他数字” 之间用0来填充;
下面我们就根据总结出来的规律编写解析代码:
int process_byte(int nums)
{
int i;
u8 a=0;
u8 byte=0;
u8 new_array[8];
memset(new_array,0,sizeof(new_array));
for(i=0;i<=nums;i++) //1...7
{
timerecode
+=ONE_BIT_TIME/2;
timerecode/=ONE_BIT_TIME;
}
if(timerecode[nums]>=9) //计算下标是从1开始的,并且去掉了9,所以剩下的也不多了
nums--;
if(nums<=0) //全0特殊处理
return 0;
find_max=nums; //去掉第一个0
/*
假设收到的是0,2,4,9
经过处理后就剩下2,4
将2放到new_array下标为1的地方,4放到下标为3的地方
*/
for(i=0;i
{
find_array=timerecode[i+1];
if(find_array!=0)
{
new_array[find_array-1]=find_array;
}
}
/*
然后在for(i=find_array[0]-1;i<8;i++)
这个循环里找,如果对应的位有边沿改变,就改变a的值
找不到就复制a的值,这里有点要注意,i循环是从高电平
开始的
*/
for(i=find_array[0]-1;i<8;i++)
{
byte>>=1;
if(i==new_array-1)
a=!a;
if(a)
byte|=0x80;
}
return byte;
}
对于上图0x37的例子,当定时器3中断时,就调用process_byte(nums-1),其中nums=6,就是边沿次数,来解析我们接收到的byte啦.
下面的截图是我用模拟串口编写的收发例子:```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190912085041489.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzMxMzY5Nw==,size_16,color_FFFFFF,t_70)
收发48个任意字节都正常,没有哪个是解析出错的。
发送部分:这部分完全依赖定时器7了,定时周期104us。
发送的原理很简单,我这里没有使用delay方式来发送,而是在定时器里实现发送的;
对于上图0x37的例子,当定时器3中断时,就调用process_byte(nums-1),其中nums=6,就是边沿次数,来解析我们接收到的byte啦.下面的截图是我用模拟串口编写的收发例子:```![在这里插入图片描述](?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzMxMzY5Nw==,size_16,color_FFFFFF,t_70)收发48个任意字节都正常,没有哪个是解析出错的。发送部分:这部分完全依赖定时器7了,定时周期104us。发送的原理很简单,我这里没有使用delay方式来发送,而是在定时器里实现发送的; #define VIR_TXBUFF_SIZ 128
#define VIR_RXBUFF_SIZ 128
typedef struct {
//---------Rx------------
u8 rxov;
u8 rxlen;
u8 rxbuff_idx;
u8 rx_decode_flag;
u8 RxBuff[VIR_RXBUFF_SIZ];//大小一定要是2的次方关系
u8 RXREG;
//---------Tx------------
u8 send_flag;
u8 send_max;
u8 send_cnt;
u8 send_mode; //0-阻塞式发送 1-中断发送(放到sendbuff里,指定send_max即可)
u8 sendbuff[VIR_TXBUFF_SIZ];
u8 TXREG;
}VIRTUAL_UART_t;
static u8 send_a_byte(u8 dat)
{
if(VirtualUart.send_flag)
return 1;
VirtualUart.TXREG=dat;TIM7->CR1 |= TIM_CR1_CEN;Tx_Pin=0;VirtualUart.send_flag=1;return 0; }
static void send_remain_byte(void)
{
if(VirtualUart.send_cnt>=VirtualUart.send_max)
{
VirtualUart.send_flag=0; //发送完毕
}
else
{
VirtualUart.TXREG=VirtualUart.sendbuff[VirtualUart.send_cnt++];
Tx_Pin=0; //产生START信号
}
}
//TIM7定时器里调用
static u8 tim_send_byte(void (*Callback)(void))
{
static int sendidx=0;
sendidx++;
if(sendidx<=8) //DATA 1,2,3...8{ Tx_Pin=VirtualUart.TXREG&0x01; VirtualUart.TXREG>>=1; } else if(sendidx==9) { //STOP Tx_Pin=1;} else if(sendidx==10) { //STOP的发送完毕了 sendidx=0; if(Callback!=NULL) //有多个字节要发送,调用回调函数继续发送下一个字节 Callback(); else VirtualUart.send_flag=0; //已经发送完毕了 return 0;}return 1; //1-busy }
//阻塞式发送
void vu_send_string(u8 *s)
{
VirtualUart.send_mode=0;
while(*s)
{
while(VirtualUart.send_flag!=0);
send_a_byte(*s);
s++;
}
}
//阻塞式发送
void vu_send_len(u8 *s,int len)
{
VirtualUart.send_mode=0;
while(len–)
{
while(VirtualUart.send_flag!=0);
send_a_byte(*s);
s++;
}
}
//中断发送
int vu_send_some_byte_noblock(int len)
{
if(VirtualUart.send_flag)
return -1;
if(len>VIR_TXBUFF_SIZ)
return -2;
VirtualUart.send_mode=1;VirtualUart.TXREG=VirtualUart.sendbuff[0];TIM7->CR1 |= TIM_CR1_CEN;Tx_Pin=0; //产生START信号VirtualUart.send_flag=1;VirtualUart.send_max=len;VirtualUart.send_cnt=1;return 0; }
//主要发送部分代码就在这儿了
void TIM7_IRQHandler(void)
{
TIM7->SR = (uint16_t)~TIM_IT_Update;
if(VirtualUart.send_mode) { if(tim_send_byte(send_remain_byte)==0) { if(VirtualUart.send_flag==0) TIM7->CR1 &= ~TIM_CR1_CEN; TIM7->CNT=0; }}else { if(tim_send_byte(NULL)==0) { TIM7->CR1 &= ~TIM_CR1_CEN; TIM7->CNT=0; }} }
网上有好多关于利用IO口来实现串口数据收发的实例,这种方法的实质都是可以应用于任何一款微处理器上,而不仅仅局限于stm32。有相关的源代码链接参考:
当然,如果成本要求不高,可以利用串口扩展芯片是最方便的,如CH438,WK系列芯片等。
IO口模拟串口的一些基本原理,阐述如下:
STM32单片机一般少则3个串口,多则5个,而项目还偏偏5个硬件串口还是不够用.板子上有几个预留IO口,可以用来模拟串口. 模拟串口一般都选9600,速度最快试了也才19200,所以限制还是较多的,一般不得以情况下才会用到.
IO口模拟串口的思路也比较简单,一切按照串口协议进行操作即可。对于发送,计算好不同波特率对应的延时时间进行数据发送。对于接收,稍微复杂。通过外部中断检测接收管脚的下降沿,检测到起始信号后开启定时器,定时器按照波特率设定好时间,每隔一段时间进入定时器中断接收数据,完成一个字节后关闭定时器。
模拟串口分收和发:收比较难,发送比较容易,那就先将接收这块吧。 接收:
有2种思路: 一种是第一个下降沿开始启动定时器,每个bit都去采样Rx电平; 从第一个边沿开始统计时间戳,接收完10个bit后再解析. 我这边碰巧选择的是第二种方法,具体实现思路是:
① 启动定时器2用来做背景时间,定时器分频后的计数频率为1MHz,那么对于9600bps来说1个bit就是104.16us,我们取整数104就可以了;
② 再同时启动一个定时器3,定时时间为1049.5,这个定时器中断发生的时候表示 一个字节接收完毕了
我们以0x37串口通讯时序图为例, ,传输的时候是LSB 1st, Rx引脚选择边沿触发, 第1次边沿到的时候
启动定时器2,同时启动定时器3, 第2个边沿的时候将定时器2的计数值存到数组里,此时是104,第3个
边沿触发的时候将其计数1045计数值,同理将4,5,6三个边沿对应的TIM2值存到缓存里,最终是 0,104,416,520,728,936,除以104变成0,1,4,5,7,9;
如何将0,1,4,5,7,9解析成0x37,看似简单的问题其实还是有点麻烦的~ 给个思路,先将0和9这样的数字去掉,变成1,4,5,7;然后从1开始数到8,跟数组里元素是否有匹配,如果匹配就将状态取反,没有就维持之前的状态:1有,2,3没有,4有,5有,6没有,7有,8没有–>11101100->倒序后就是0x37了;
再来分析几个处理后的值:4,5,7: 1,2,3没有,4有,5有,6没有,7有,8没有–>00010011->逆序后11001000->0xC8 1,2: 1有,2有,3,4,5,6,7,8没有->10000000->逆序后00000001->0x01
1,2,3,4,6,7,8:1有,2有,3有,4有,5没有,6有,7有,8有->10100101->逆序后10100101->0xA5
7,8:1,2,3,4,5,6没有,7有,8有->00000010->逆序后01000000->0x40
总结下如果第一个是1开头的那么得出的bit就是1,如果第一个是其他数字开头的,则 “1到其他数字” 之间用0来填充;
下面我们就根据总结出来的规律编写解析代码:
int process_byte(int nums)
{
int i;
u8 a=0;
u8 byte=0;
u8 new_array[8];
memset(new_array,0,sizeof(new_array));
for(i=0;i<=nums;i++) //1...7
{
timerecode
+=ONE_BIT_TIME/2;
timerecode/=ONE_BIT_TIME;
}
if(timerecode[nums]>=9) //计算下标是从1开始的,并且去掉了9,所以剩下的也不多了
nums--;
if(nums<=0) //全0特殊处理
return 0;
find_max=nums; //去掉第一个0
/*
假设收到的是0,2,4,9
经过处理后就剩下2,4
将2放到new_array下标为1的地方,4放到下标为3的地方
*/
for(i=0;i
{
find_array=timerecode[i+1];
if(find_array!=0)
{
new_array[find_array-1]=find_array;
}
}
/*
然后在for(i=find_array[0]-1;i<8;i++)
这个循环里找,如果对应的位有边沿改变,就改变a的值
找不到就复制a的值,这里有点要注意,i循环是从高电平
开始的
*/
for(i=find_array[0]-1;i<8;i++)
{
byte>>=1;
if(i==new_array-1)
a=!a;
if(a)
byte|=0x80;
}
return byte;
}
对于上图0x37的例子,当定时器3中断时,就调用process_byte(nums-1),其中nums=6,就是边沿次数,来解析我们接收到的byte啦.
下面的截图是我用模拟串口编写的收发例子:```
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190912085041489.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzMxMzY5Nw==,size_16,color_FFFFFF,t_70)
收发48个任意字节都正常,没有哪个是解析出错的。
发送部分:这部分完全依赖定时器7了,定时周期104us。
发送的原理很简单,我这里没有使用delay方式来发送,而是在定时器里实现发送的;
对于上图0x37的例子,当定时器3中断时,就调用process_byte(nums-1),其中nums=6,就是边沿次数,来解析我们接收到的byte啦.下面的截图是我用模拟串口编写的收发例子:```![在这里插入图片描述](?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MzMxMzY5Nw==,size_16,color_FFFFFF,t_70)收发48个任意字节都正常,没有哪个是解析出错的。发送部分:这部分完全依赖定时器7了,定时周期104us。发送的原理很简单,我这里没有使用delay方式来发送,而是在定时器里实现发送的; #define VIR_TXBUFF_SIZ 128
#define VIR_RXBUFF_SIZ 128
typedef struct {
//---------Rx------------
u8 rxov;
u8 rxlen;
u8 rxbuff_idx;
u8 rx_decode_flag;
u8 RxBuff[VIR_RXBUFF_SIZ];//大小一定要是2的次方关系
u8 RXREG;
//---------Tx------------
u8 send_flag;
u8 send_max;
u8 send_cnt;
u8 send_mode; //0-阻塞式发送 1-中断发送(放到sendbuff里,指定send_max即可)
u8 sendbuff[VIR_TXBUFF_SIZ];
u8 TXREG;
}VIRTUAL_UART_t;
static u8 send_a_byte(u8 dat)
{
if(VirtualUart.send_flag)
return 1;
VirtualUart.TXREG=dat;TIM7->CR1 |= TIM_CR1_CEN;Tx_Pin=0;VirtualUart.send_flag=1;return 0; }
static void send_remain_byte(void)
{
if(VirtualUart.send_cnt>=VirtualUart.send_max)
{
VirtualUart.send_flag=0; //发送完毕
}
else
{
VirtualUart.TXREG=VirtualUart.sendbuff[VirtualUart.send_cnt++];
Tx_Pin=0; //产生START信号
}
}
//TIM7定时器里调用
static u8 tim_send_byte(void (*Callback)(void))
{
static int sendidx=0;
sendidx++;
if(sendidx<=8) //DATA 1,2,3...8{ Tx_Pin=VirtualUart.TXREG&0x01; VirtualUart.TXREG>>=1; } else if(sendidx==9) { //STOP Tx_Pin=1;} else if(sendidx==10) { //STOP的发送完毕了 sendidx=0; if(Callback!=NULL) //有多个字节要发送,调用回调函数继续发送下一个字节 Callback(); else VirtualUart.send_flag=0; //已经发送完毕了 return 0;}return 1; //1-busy }
//阻塞式发送
void vu_send_string(u8 *s)
{
VirtualUart.send_mode=0;
while(*s)
{
while(VirtualUart.send_flag!=0);
send_a_byte(*s);
s++;
}
}
//阻塞式发送
void vu_send_len(u8 *s,int len)
{
VirtualUart.send_mode=0;
while(len–)
{
while(VirtualUart.send_flag!=0);
send_a_byte(*s);
s++;
}
}
//中断发送
int vu_send_some_byte_noblock(int len)
{
if(VirtualUart.send_flag)
return -1;
if(len>VIR_TXBUFF_SIZ)
return -2;
VirtualUart.send_mode=1;VirtualUart.TXREG=VirtualUart.sendbuff[0];TIM7->CR1 |= TIM_CR1_CEN;Tx_Pin=0; //产生START信号VirtualUart.send_flag=1;VirtualUart.send_max=len;VirtualUart.send_cnt=1;return 0; }
//主要发送部分代码就在这儿了
void TIM7_IRQHandler(void)
{
TIM7->SR = (uint16_t)~TIM_IT_Update;
if(VirtualUart.send_mode) { if(tim_send_byte(send_remain_byte)==0) { if(VirtualUart.send_flag==0) TIM7->CR1 &= ~TIM_CR1_CEN; TIM7->CNT=0; }}else { if(tim_send_byte(NULL)==0) { TIM7->CR1 &= ~TIM_CR1_CEN; TIM7->CNT=0; }} }
举报