历史上的今天
返回首页

历史上的今天

今天是:2025年06月11日(星期三)

正在发生

2020年06月11日 | STM8学习笔记----普通IO口模拟串口功能

2020-06-11 来源:eefocus

串口在产品应用中很常见,但是单片机的默认带的串口往往比较少,有时候就会出现串口不够用,所以就想着能不能用普通IO口模拟串口来实现串口的功能。


要模拟串口首先要清楚串口数据传输过程中的原理。

在这里插入图片描述

常用的串口格式为 1位起始位,8位数据位,无校验位,1位结束位。起始位为低电平,结束位为高电平。数据0为低电平,数据1为高电平。


所以最简单的串口传输一个字节总共有10个电平变化,每个电平的宽度由波特率决定的。


具体的串口数据分析,可以参考这篇文章:STM8学习笔记---通过示波器分析串口数据。


下面看一个通过波特率如何计算每个位的电平宽度。


发送一个字节,以stm8中9600bit/s的波特率计算的过程为例(1秒钟传输9600位)。

可以计算出传输1位所需要的时间 T1 = 1/9600 约为104us。


通过计算可以看出来,如果波特率为9600时,一个位的电平宽度要为104us。

在这里插入图片描述

上图中就是一个字节的完整波形,起始位为低电平,结束位为高电平,中间8位为数据位,无校验位。数据位是低位在前,高位在后。也就是和起始位挨着的是最低位,和结束位挨着的是最高位。通过这个波形就可以分析出,发送的数据是0x00。


下面在看一看0X01的波形。

在这里插入图片描述

起始位为低电平,结束位为高电平,中间数据位有一个高电平,其余都是低电平,按照低位在前,高位在后规律,数据位就是0000 0001 刚好是16进制的0x01。


知道了串口数据的格式,下面就开始写代码,先写串口发送代码。


//输出1

void Send_1( void )

{

    SIM_TXD = 1;

}

//输出0

void Send_0( void )

{

    SIM_TXD = 0;

}

//发送一个字节

void WriteByte( unsigned char sdata ) //波特率9600

{

   //如果数据误码率比较高,可以修改delay_us延时时间

    unsigned char i;

    unsigned char value = 0;

    //发送起始位

    Send_0();

    delay_us( 100 );

    //发送数据位

    for( i = 0; i < 8; i++ )

    {

        value = ( sdata & 0x01 ); //先传低位

        if( value )

        {

            Send_1();

        }

        else

        {

            Send_0();

        }

        delay_us( 100 );        //经测试延时100us 数据没有误差 示波器上观察波形时间为104us左右

        sdata = sdata >> 1;

    }

    //停止位

    Send_1();

    delay_us( 100 );           

}


首先发送起始位,将IO口电平拉低,延时104us,下来发送8位数据位,低位在前,高位在后,每发送一位就延时104us。最后发送结束位,将IO口电平置高,在延时104us。这样一个字节就发送结束了。

由于延时程序使用软件实现的,所以延时精度不是很高,延时参数设置为100时,通过示波器看一位刚好是104us。由于不同的编译器,不同的单片机,通过软件计算的延时时间不同,所以这里的延时时间最好通过示波器来测,延时时间的精确度决定了通信的误码率。延时时间精确通信误码率就低,延时误差比较大,通信误码率就高。


下面再看接收程序


//接收一个字节

void ReadByte( void )

{

    unsigned char i, value = 0;

    if( !SIM_RXD )    //RXD_IN RXD等于0时开始接收

    {

        //等过起始位 起始位为低电平

        delay_us( 100 );

        //接收8位数据位

        for( i = 0; i < 8; i++ )

        {

            value >>= 1;

            if( SIM_RXD )      //RXD_IN

            {

                value |= 0x80;

            }

            delay_us( 100 );

        }

        //等过结束位 结束位为高电平

        //delay_us(100);  //一次性接收大量数据时,防止程序执行代码时浪费时间,在结束符时可以不用等待

        RecFlag = 1;      //标记已经接收到了数据

        RecBuf = value;

        return;

    }

}


接收数据就更好理解了,读取IO口的电平,如果出现低电平就开始接收数据,然后读取8个数据位的电平,在等待结束位结束。这样一个字节的数据就接收完成了。


那么如何判断什么时候去接收串口的数据呢?


有两种方式去实现,一种是在死循环中用查询方式去判断,一直读取IO的的电平,如果出现低电平就认为串口有数据发送进来了。


实现代码如下:


//查询方式接收数据

void ReadString( void )

{

    unsigned int cnt = 0, i = 0, j = 0;

    unsigned char recstr[100] = {0};

    _Bool send_F = 0;

    while( 1 )

    {

        if( !RecFlag )            //未收到数据时扫描

        {

            ReadByte();          //扫描数据

            cnt++;              //统计扫描次数

        }

        else if( RecFlag )        //收到数据后读取数据 如果接收到数据没有读取时 不会继续接收数据

        {

            cnt = 0;

            RecFlag = 0;

            recstr[i++] = RecBuf; //存储接收的数据

            RecBuf = 0;

            send_F = 1;            //标记数据可以发送

        }

        if( ( cnt >= 100 ) && ( send_F == 1 ) ) //扫描次数超过100 并且可以发送数据

        {

            cnt = 0;

            WriteString( recstr );  //发送接收到的数据

            send_F = 0;             //清除发送标志

            i = 0;                  //清除接收数据数组下标

            for( j = 0; j < 100; j++ ) //清除接收数据缓冲区

            {

                recstr[j] = 0;

            }

            return;                 //发送完数据后返回

        }

        else if( cnt > 500 )        //没有接收到数据 超时退出

        {

            cnt = 0;

            return;

        }

    }

}


这种方式实现起来比较简单,但是对于程序编写比较麻烦,因为要一直监视者IO口,所以程序干其他事情时,很有可能错过数据的接收。可以用第二种方式,IO口中断来判断什么时候要开始接收数据,将IO口设置为下降沿中断,当有下降沿出现时,说明串口有数据进来了,然后再去读取串口数据。没有中断发生时,程序就可以干其他事情了。


实现代码如下:


//通道PC3口的下降沿中断检测数据

//PC3口中断 RXD

#pragma vector = 7                  // IAR中的中断号,要在STVD中的中断号上加2

__interrupt void RXDInterrupt( void )

{

    PC_CR2 &= ~( 1 << 3 );      //禁止外部中断

    ReadByte();

    if( recEnd == 0x01 )

    {

        if( RecBuf == 0x0a )      //收到结束符 0x0a 标记数据接收完毕

        {

            recEnd |= 0x02;

            recCNT = 0;

        }

    }

    if( recEnd != 0x03 )

    {

        if( RecBuf != 0x0d )        //结束符为回车换行符 0x0d 0x0a

        {

            recBUFF[recCNT++] = RecBuf; //没收到结束符存储数据

            RecBuf = 0;

        }

        else if( RecBuf == 0x0d )   //收到0x0d 标记结束符开始

        {

            recEnd |= 0x01;

        }

    }

    PC_CR2 |= ( 1 << 3 );       //使能外部中断

}


当出现下降沿之后进入中断程序,这时候要关闭外部中断,开始读取IO口电平状态。若不关闭中断,在读取IO电平的过程中中断还会不停的进入,这样就会影响读取数据的准确性。所以进入中断会首先要关闭中断,接收完一个字节之后,在打开中断,接收下一个字节。直到收到了回车换行符(也就是0x0D 0x0A),就认为数据发送已经结束。就退出接收过程,然后主程序就可以去处理接收到的数据了。


这样串口的发送和接收通过IO的电平模式就可以实现了。


看一下测试效果

部分参考代码如下:


模拟发送和接收代码:


#include "myuart.h"

unsigned char recBUFF[100] = {0};       //存储接收到的数据

unsigned char recCNT = 0;               //接收数据个数

unsigned char recEnd = 0;               //数结束标志

unsigned char RecBuf;         //接收缓冲区

_Bool RecFlag = 0;                      //接收到数据标志位

 

//模拟串口初始化 PC3 RXD  PC4 TXD

void MyUart_Init ( void )

{

    PC_DDR |= ( 1 << 4 ); //PC4 输出 TXD

    PC_CR1 |= ( 1 << 4 ); //PC4 推挽输出

    PC_CR2 |= ( 1 << 4 );

 

    PC_DDR &= ~( 1 << 3 ); //PC3 输入 RXD

    PC_CR1 &= ~( 1 << 3 ); //PC3

    PC_CR2 |= ( 1 << 3 );               //使能外部中断

 

    EXTI_CR1 |= ( 1 << 5 );             //PC口下降沿触发

}

//输出1

void Send_1( void )

{

    SIM_TXD = 1;

}

//输出0

void Send_0( void )

{

    SIM_TXD = 0;

}

//发送一个字节

//以stm8中9600bit/s的波特率计算的过程为例(1秒钟传输9600位)。

//可以计算出传输1位所需要的时间 T1 = 1/9600 约为104us。

void WriteByte( unsigned char sdata ) //波特率9600

{

   //如果数据误码率比较高,可以修改delay_us延时时间

    unsigned char i;

    unsigned char value = 0;

    //发送起始位

    Send_0();

    delay_us( 100 );

    //发送数据位

    for( i = 0; i < 8; i++ )

    {

        value = ( sdata & 0x01 ); //先传低位

        if( value )

        {

            Send_1();

        }

        else

        {

            Send_0();

        }

        delay_us( 100 );        //经测试延时100us 数据没有误差 示波器上观察波形时间为104us左右

        sdata = sdata >> 1;

    }

    //停止位

    Send_1();

    delay_us( 100 );           

}

//发送字符串

void WriteString( unsigned char *s )

{

    while( *s != 0 )

    {

        WriteByte( *s );

        s++;

    }

}

 

//接收一个字节

void ReadByte( void )

{

    unsigned char i, value = 0;

    if( !SIM_RXD )    //RXD_IN RXD等于0时开始接收

    {

        //等过起始位 起始位为低电平

        delay_us( 100 );

        //接收8位数据位

        for( i = 0; i < 8; i++ )

        {

            value >>= 1;

            if( SIM_RXD )      //RXD_IN

            {

                value |= 0x80;

            }

            delay_us( 100 );

        }

推荐阅读

史海拾趣

ABECO公司的发展小趣事

随着业务的发展,ABECO在1970年做出了一个大胆的决定——进军马耳他市场。这一决策既带来了挑战,也带来了机遇。公司需要适应新的市场环境,了解当地的需求和文化。经过一段时间的摸索和努力,ABECO凭借其高品质的产品和专业的服务,成功在马耳他市场打开了局面,为后续的发展奠定了坚实的基础。

Glorious Sources Co Ltd公司的发展小趣事
采取必要的抗干扰措施,如屏蔽、接地等,以提高电路的稳定性和可靠性。
ERGOBAHCO公司的发展小趣事

随着公司规模的扩大,ERGOBAHCO公司逐渐意识到品质与创新对于品牌的重要性。公司投入大量资金引进先进的生产设备和技术,并建立了严格的质量管理体系。同时,ERGOBAHCO公司还鼓励员工进行创新研发,不断推出具有市场竞争力的新产品。这些举措使得ERGOBAHCO公司的产品逐渐在行业内树立了良好的口碑,品牌知名度也随之提升。

台湾富致(FUZETEC)公司的发展小趣事

为了更好地服务于中国市场,Futaba在2002年成立了双叶电子科技开发(北京)有限公司。该公司专营Futaba的全线产品,包括VFD真空荧光显示管和RC无线遥控设备等。这一举措标志着Futaba在全球化布局上迈出了重要一步,也为公司在中国市场的快速发展奠定了基础。北京分公司的成立不仅加强了Futaba与中国客户的联系,还促进了公司在技术、产品和市场等方面的全方位合作。

Advanced Fibreoptic Engineering Ltd公司的发展小趣事

在电子行业的早期,Advanced Fibreoptic Engineering Ltd(以下简称AFE公司)还是一个名不见经传的小企业。然而,随着技术的不断进步,AFE公司凭借其在光纤技术领域的深厚积累,成功研发出了一种具有划时代意义的新型光纤材料。这种材料不仅传输速度快,而且损耗极低,极大地提高了数据传输的效率和质量。这一技术突破迅速为AFE公司赢得了市场认可,公司的订单量激增,业绩逐年攀升。

随着技术的推广和应用,AFE公司的光纤产品逐渐在通信、医疗、工业等多个领域得到广泛应用。公司不仅在国内市场占据了一席之地,还积极拓展海外市场,与国际知名企业建立了稳定的合作关系。凭借卓越的产品性能和良好的市场口碑,AFE公司逐渐在电子行业中崭露头角,成为了光纤技术领域的佼佼者。

以上是第一个故事的示例,若您想要探索更多关于AFE公司的发展故事,请输入继续。

(注:由于我无法实时获取具体公司的实际发展故事,以上故事为虚构内容,仅用于展示故事编写风格和结构。如果您需要真实、具体的故事,请提供更多关于AFE公司的信息,以便我能为您编写更贴近实际的内容。)

HTC Korea(TAEJIN Technology )公司的发展小趣事

进入安卓时代,HTC再次展现了其敏锐的市场洞察力和强大的技术实力。2008年,HTC联合电信运营商T-Mobile推出了世界上第一款安卓手机T-Mobile G1,这款手机的成功标志着HTC正式进入了安卓阵营。随后,HTC推出了多款备受好评的安卓手机,如HTC Hero(G3)等,进一步巩固了其在智能手机市场的地位。与谷歌和安卓的合作,为HTC带来了前所未有的发展机遇。

问答坊 | AI 解惑

汽车设计制造全过程

对于大多数人来说,对车的欣赏基本都是整车,除了性能之外,汽车的外观和内饰是人们谈论最多的话题,因为这是对一辆车最直观的印象,所谓汽车设计,简单的理解是根据一款车型的多方面要求来设计汽车的外观及内饰,使其在充分发挥性能的基础上艺术化 ...…

查看全部问答>

ARM初学者入门手册 .pdf

大家分享一下,觉得还不错…

查看全部问答>

【压缩包】121篇FPGA 图像处理相关论文大赠送

应坛友要求做成压缩包方便大家下载!其实我的本意是让大家有选择的下的,没想到大家都求知若渴想要都下下来…

查看全部问答>

程序初始化进入HardFault_Handler

如题:IAR+Jlink,SWD方式调试,芯片型号为STM32F101CBT6,仿真运行,执行到函数TIM2_Configuration中的TIM_Cmd(TIM2, ENABLE);就进入到HardFault_Handler,如果复位后重新运行则执行到NVIC_Configuration中的NVIC_Init(&NVIC_InitStructure ...…

查看全部问答>

LTC1700芯片发热...不接负载电流达到150mA左右。。

本帖最后由 paulhyde 于 2014-9-15 09:09 编辑 如题,电路图pdf典型解法。其中输出端电容取的是470u/16V 反馈电阻取的300K 其他基本跟典型参数一样。后改进过电感600uh。效果一样的。不知道问题在哪里了。。。谢谢懂的朋友哈  …

查看全部问答>

有一款输出正负12V和正负5V芯片

有一款能输出正负5V和正负12V的电源芯片,在哪里见过,忘了型号了,哪位知道有哪些芯片可以输出多路双电源芯片…

查看全部问答>

我是新手,想学习PCB。应该怎么做?

我是新手,想学习PCB。应该怎么做?求高手指点。…

查看全部问答>

74hc373锁存异常

用2片373扩展IO口驱动多路继电器,输出与仿真结果完全不同 两片373分别叫Q1和Q2吧,Q1和Q2和数据输入口接51芯片P1口,LE分别接P3.0和P3.1使能,QE全部使能,起始P3.0和P3.1输出0,P1输出一个字节,Q1使能,关Q1;输出下个字节,Q2使能,关Q2。这样 ...…

查看全部问答>

ADXL362: 微功耗、3轴、±2 G/±4 G/±8 G数字输出MEMS加速度计

ADXL362:   微功耗、3轴、±2 G/±4 G/±8 G数字输出MEMS加速度计             ADXL362是一款超低功耗、3轴MEMS加速度计,输出数据速率 为100 Hz时功耗低于2 μA,在运动触发唤醒模式下功耗 ...…

查看全部问答>