2019-11-04 22:26:31 lin5103151 阅读数 223
  • 串口通信和RS485-第1季第13部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第13个课程,主要讲解了串行通信UART及其扩展RS485。本课程很重要,因为串口通信是我们接触的早也简单的通信方式,是后续继续学习SPI、I2C甚至USB、网络通信等的基础,大家务必认证对待完全掌握。

    6010 人正在学习 去看看 朱有鹏

串口,作为单片机程序开发中最常用、最方便,也是应用最广泛的程序调试方法;无论是作为调试工具,打印出调试信息,还是对功能模块进行通信,串口是每个单片机开发人员最常用的单片机外设。
由于大部分51单片机不支持在线调试功能,所以串口作为一种有效的调试功能,所以在51单片机程序开发时,无法进行在线调试,不妨可以多采用串口来进行调试。
1.串口配置

51单片机配置除了需要配置2个8位寄存器SCON、PCON外,还要配置定时器1的控制寄存器TMOD,因为串口在进行收发需要使用定时器来采样。
(1)状态控制寄存器 SCON
SCON 是一个逐位定义的 8 位寄存器,用于控制串行通信的方式选择、接收和发送,指 示串口的状态,SCON 即可以字节寻址也可以位寻址,字节地址 98H,地址位为 98H~9FH。 它的各个位定义如下:
在这里插入图片描述
SM0 和 SM1 是串口的工作方式选择位,2 个选择位对应 4 种工作方式,如下表,其中 Fosc 是振荡器的频率。
在这里插入图片描述
(2)电源与波特率控制寄存器
控制寄存器 PCON 也是一个逐位定义的 8 位寄存器,目前仅仅有几位有定义,如下所示:
在这里插入图片描述
仅最高位 SMOD 与串口的控制有关,其他位与掉电方式有关。PCON 的地址为 87H 只能按 字节寻址,SMOD 是串行通信波特率系数控制位,当串口工作在工作方式 1、2 时,若使用 T1 作为波特率发生器其 SMOD=1 则波特率加倍。
(3)定时器控制模式寄存器
TMOD是定时器、计数器模式控制寄存器,它是一个逐位定义的8为寄存器,但只能使用字节寻址
在这里插入图片描述
当串口工作在工作方式0和2是,波特率固定,方式0时fosc/12;方式2时fosc/32或fosc/64(根据SMOD判断)。当串口工作在方式1时,波特率=(2^SMOD/32)*(单片机时钟频率/(256-X)),X是初值;C/T#为定时器和计数器选择位,0为定时器,1为计数器

2.串口通用程序
为了有较好的通用性,将串口程序配置为一个H文件和C文件,往后只要开发51单片机程序,都可以将该两个文件复制在工程中直接使用,无须修改。
(1)uart.H

#ifndef _UART_H_
#define _UART_H_

#include "reg52.h"

void UartInit(void);
void Send_string(unsigned char *c);
void Send_Data(unsigned char DAT);

#endif

(2)uart.c

#include "uart.h"

//串口初始化
void UartInit(void)
{
	/*串口初始化 */
	TMOD = 0x20;	  //定时器T1使用工作方式2
	SCON = 0x50;	  //工作方式1,,允许接收   
	PCON = 0x10;
	TH1 = 253;        // 设置初值,波特率9600bps
	TL1 = 253;
	TR1 = 1;          // 开始计时	
	ES = 1;         //打开接收中断
	EA = 1;           // 打开所以中断   
	TI = 0;
	RI = 0;
}

void Send_Data(unsigned char DAT)
{
	ES = 0;
	TI=0;
	SBUF = DAT ;
	while(TI==0);
	TI=0;
	ES = 1;
}

void Send_string(unsigned char  *c)
{
	while(*c != '\0')
	{
		Send_Data(*c++);
	}	
}

void RSINTR() interrupt 4 using 2
{
	EA=0;
	if(TI==1) //发送中断	  
	{
		TI=0;
	}

	if(RI==1)	 //接收中断		  
	{	

		RI=0;								 

	}
	EA=1;
}

如需串口参考例程, 请关注公众号,首页回复“串口”获取资料
在这里插入图片描述

2018-07-06 15:42:52 lijinshanba 阅读数 2462
  • 串口通信和RS485-第1季第13部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第13个课程,主要讲解了串行通信UART及其扩展RS485。本课程很重要,因为串口通信是我们接触的早也简单的通信方式,是后续继续学习SPI、I2C甚至USB、网络通信等的基础,大家务必认证对待完全掌握。

    6010 人正在学习 去看看 朱有鹏

讲道理都tm大三暑假了还搞51单片机而且关键是还遇到了一个问题解决了一天才解决出来真的是很丢人了。

不过我会珍惜这个机会的,毕竟这有可能是我辈子最后一次跟着老师搞这个破玩意了。

好了不废话了 收!

昨天遇到的问题是单片机用串口给电脑发数据,本人用串口助手查看时乱码了,发送的数据是英文和数字,没有存在中文,马上进行问题排除:

解决方案一:printf的原因吗?

代码里用piintf函数来进行串口发送数据,可能printf只适合于stm32不能适用于51,但是仔细一想又不对啊,我tm大三上的单片机大作业就是用printf函数进行串口发送数据的啊,怎么当时就可以啊。于是我把当时的代码拿过来修改,还是不行。

解决方案一卒。

解决方案二:硬件芯片的原因吗?

换了另外一块芯片,还是不行。

解决方案二卒。

解决方案三:晶振的原因吗?

赶紧查看开发板上面的晶振,刻着12Mhz,在keil4软件里面查看晶振,晶振的设置是12Mhz,这里算是对上了。想起上个学期的单片机晶振是11.0592Mhz,可能是这个原因吧,但是没有办法啊,晶振已经焊上去了,我能怎么办我也很绝望啊。

解决方案三卒。

解决方案四:波特率的原因吗?

这里要感谢一篇博客给我的启发,详见参考资料。

和博主一样,使用波特率9600,TL1和TH1都设置成0xfd,不管是中文还是英文都是乱码,那肿么办呢?波特率换成2400,2400,2400,重要的事情说三遍!!!TL1和TH1都设置成0xf3。

哈哈哈哈终于不乱码了,开心。


总结:没什么好总结了,就是菜。就是一个菜鸡发现了9600到2400的过程。

参考资料:

https://www.cnblogs.com/geekalan/p/4005427.html

https://zhidao.baidu.com/question/110468495.html?fr=qrl&index=3

2019-07-15 14:53:28 u014779536 阅读数 135
  • 串口通信和RS485-第1季第13部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第13个课程,主要讲解了串行通信UART及其扩展RS485。本课程很重要,因为串口通信是我们接触的早也简单的通信方式,是后续继续学习SPI、I2C甚至USB、网络通信等的基础,大家务必认证对待完全掌握。

    6010 人正在学习 去看看 朱有鹏

背景

  最近做单片机开发经常遇见要用串口接收数据的情况,实际项目中肯定不能当串口接收中断一来就去处理,于是我们可以用到队列这个数据结构来保存上一帧数据,想用的时候取出即可。

开始

  1. 新建队列结构体
#define BUFFER_MAX 20		//队列缓冲区大小,根据实际情况来定

typedef struct uart_queue{
    unsigned char head;						//队列头指针
    unsigned char tail;						//队列尾指针
    unsigned char buffer[BUFFER_MAX];		//队列缓存数组
	unsigned char RXReceiving_Flag;			//正在接收标志
	unsigned char RXFinish_Flag;			//一帧数据接收完成标志
	unsigned char RXTimer_Cnt;				//字节接收间隔计时
	unsigned char RXLength;					//接收自己总长度
}UART_QueueTypeDef;

UART_QueueTypeDef uart0_queue;	//新建一个队列结构体

接收机制说明:

  当数据入队后,队列尾指针+1,当数据出队后,队列头指针+1;

  当一帧数据完成后,RXLength记录长度,用户可以取出相应数据,进行处理。

  1. 建立出/入队函数
//入队函数
unsigned char uart_enqueue(const unsigned char _buf)
{
    //如果尾指针小于最大缓存,表示还可以入队
    if(uart0_queue.tail < BUFFER_MAX)
    {
        uart0_queue.buffer[uart0_queue.tail++] = _buf;
        uart0_queue.RXLength++;		//入队后数据长度加1
        return 1;					//入队成功
    }
    else
    {
        return 0;					//入队失败
    }
}

//出队函数
unsigned char uart_dequeue(unsigned char * _buf)
{
    //如果头指针不等于尾指针,表示还是有数据可以出队
    if(uart0_queue.head != uart0_queue.tail)	
    {
        *_buf = uart0_queue.buffer[uart0_queue.head++];		//从数组中获取数据
        uart0_queue.RXLength--;								//入队后数据长度减1
        return 1;		//出队成功
    }
    else
    {
        return 0;		//出队失败
    }
}
  1. 队列初始化函数
//将队列全部设为0
void reset_uart_queue(void)
{
    unsigned char i;
    
    for(i=0; i<BUFFER_MAX; i++)
    {
        uart0_queue.buffer[i] = 0x00;
    }
    uart0_queue.head = 0;
    uart0_queue.tail = 0;    
    uart0_queue.RXLength = 0;
}
  1. 指定个数数据取出函数
//从串口缓冲队列取出指定个数数据
unsigned char POP_uart_queue(unsigned char * _data,unsigned char num)
{
    unsigned char byte = 0;
    unsigned char count = 0;
    
    while((count < num) && (uart_dequeue(&byte)))	//当长度小于num且出队成功
    {
        _data[count++] = byte;						//保存数据到数组中
    }
    
    if(uart0_queue.head != num)
        return 0;		//接收失败
    else
        return 1;		//接收成功
}
  1. 接收机制

  现在我们来理一理接收机制,当串口接收到数据之后,会产生中断,此时我们得立马取出数据,接收下一个,当一字节接收完之后,5ms内无下一字节视为一帧数据接收完成

  • 原理:假设串口波特率为9600bps,一个起始位,一个停止位,8个数据位,无校验位,总共10位数据,发送一个字节次所花时间为:(10/9600)*1000 = 1.41ms,这个意思就是在一帧数据内,字节与字节接收时间不超过1.41ms,我们暂且扩大为5ms。

  我们定义一个接收完成标志***RXFinish_Flag***,接收完一帧数据后,我们将这个标志置位1,取出后复位0,在函数主任务内轮询监控此标志状态,为1则取出数据并做出相应动作。

  期间需要计时,要用到定时器,我们先设置定时器0中断为1ms一次,定义变量***RXTimer_Cnt***来计时,

//如果正在接收,判断数据接收时间是否大于5ms
if(uart0_queue.RXReceiving_Flag)
{
    if(uart0_queue.RXTimer_Cnt++>=5)		//大于5ms表示一帧数据接收完成
    {
        uart0_queue.RXFinish_Flag = 0x01;	//接收完成标志置1
        uart0_queue.RXReceiving_Flag=0;		//正在接收标志置0
    }
}
  1. 串口中断处理
void RxIntCallback(void)
{
		unsigned char _buf;
		_buf = M0P_UART1->SBUF;				//保存uart1接收到的一个字节
    	
		//如果已经接收完一帧数据,接收完成标志还为1,说明上一帧数据未处理;
		//再次来数据表示是下一帧的,就清空上一帧,只要最新一帧数据;
		if(uart0_queue.RXFinish_Flag == 1)	
		{
			reset_uart_queue();				//初始化队列参数
		}
		uart0_queue.RXReceiving_Flag=1;		//正在接收
		uart_enqueue(_buf);					//数据入队
		uart0_queue.RXTimer_Cnt=0;			//连续接收计时清零
		
}
  1. 数据处理

主函数中采用轮询的方式查询是否接收完成一帧数据,如果接收完成则做出相应处理,此处我是把数据原封不动返回。

//新建静态数组来保存接收到的数据
static uint8_t uart_testarr[BUFFER_MAX] = {0};

while(1)
{
    if(uart0_queue.RXFinish_Flag == 1)		//如果接收完成标志为1,表示接收完成
    {
        unsigned char uart1_rx_length=0;	//定义变量来保存接收长度
        uart0_queue.RXFinish_Flag=0;		//清空接收完成标志
        uart1_rx_length = uart0_queue.RXLength;				//保存长度
        POP_uart_queue(uart_testarr, uart1_rx_length);		//取出数据
        my_uartSendString(uart_testarr, uart1_rx_length);	//串口发送数据
        reset_uart_queue();					//初始化队列参数
    }
}
2018-08-04 09:42:47 bolinste 阅读数 1357
  • 串口通信和RS485-第1季第13部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第13个课程,主要讲解了串行通信UART及其扩展RS485。本课程很重要,因为串口通信是我们接触的早也简单的通信方式,是后续继续学习SPI、I2C甚至USB、网络通信等的基础,大家务必认证对待完全掌握。

    6010 人正在学习 去看看 朱有鹏

尊重原创,转载请说明出处……

 

在工作中用单片机很久了,对通信数据的处理有一些小小的心得体会。分享出来供讨论和指正。

以下讨论基于C语言。

 

在单片机中串口是一个很重要和基本的外设。对串口数据的存储很多大拿都有自己的方式方法。个人总结有如下几种:

1、开辟一个较大的数组对数据进行保存。有时会结合定时器进行数据的“分帧”/“分包”

2、使用链表的方式进行数据存储。每个字节做成一个节点,逐节增加节点予以记录。

3、使用内存管理的方式。有经验的工程师会自己写内存管理的程序,把串口读到的数据通过内存管理进行处理。

4、使用OS的数据队列。

 

几种方式的对比分析:

编号 优点 缺点
1 写法简单,容易理解 数组长度有限,有数据溢出的风险。
2 理论实现容易,数组长度灵活 占用的内存过大
3 灵活易用,数组长度灵活,不依赖操作系统 理解起来可能会有难度
4 灵活易用,数组长度灵活,依赖操作系统 必须依赖OS

 

 

 

 

 

 

下面对上述的几种方式做详细的介绍。

 

 

2018-09-01 10:45:52 Dancer__Sky 阅读数 1582
  • 串口通信和RS485-第1季第13部分

    本课程是《朱有鹏老师单片机完全学习系列课程》第1季第13个课程,主要讲解了串行通信UART及其扩展RS485。本课程很重要,因为串口通信是我们接触的早也简单的通信方式,是后续继续学习SPI、I2C甚至USB、网络通信等的基础,大家务必认证对待完全掌握。

    6010 人正在学习 去看看 朱有鹏

概述:

     在使用STM8L101F3这款单片机时,由于它只有8K的flash,空间非常小,只要调用C库函数printf编译后整个文件很大,直接程序溢出。这也就意味着我们实现printf串口打印调试就没办法进行。既然使用不了库函数,那么我们就可以自己动手封装类似printf的函数,这样我们就可以实现数据串口打印啦。这里就直接放上我的STM8L101F3的部分源码了,希望可以给你一些参考。

源码:

#include "stdarg.h"
#include "stm8l10x.h"

void USART_Config(void)
{
    /*Set the USART RX and USART TX at high level*/
    GPIO_ExternalPullUpConfig(GPIOC,GPIO_Pin_3|GPIO_Pin_4, ENABLE);
    /* Enable USART clock */
    CLK_PeripheralClockConfig(CLK_Peripheral_USART, ENABLE);
    USART_DeInit();
    USART_Init((uint32_t)9600, USART_WordLength_8D, USART_StopBits_1,
                USART_Parity_No, (USART_Mode_TypeDef)(USART_Mode_Rx | USART_Mode_Tx));
    USART_Cmd(DISABLE);
    enableInterrupts();
    USART_Cmd(ENABLE);    
}

/*发送串口数据*/
void send_uart_data(uint8_t data)
{
  while (USART_GetFlagStatus(USART_FLAG_TXE) == RESET);
  USART_SendData8(data);
}

/*
  功能:将int型数据转为2,8,10,16进制字符串
  参数:value --- 输入的int整型数
        str --- 存储转换的字符串
        radix --- 进制类型选择
  注意:8位单片机int字节只占2个字节
*/
char *sky_itoa(int value, char *str, unsigned int radix)
{
  char list[] = "0123456789ABCDEF";
  unsigned int tmp_value;
  int i = 0, j, k = 0;
  if (NULL == str) {
    return NULL;
  }
  if (2 != radix && 8 != radix && 10 != radix && 16 != radix) {
    return NULL;
  }
  if (radix == 10 && value < 0) {
    //十进制且为负数
    tmp_value = (unsigned int)(0 - value);
    str[i++] = '-';
    k = 1;
  } else {
    tmp_value = (unsigned int)value;
  }
  //数据转换为字符串,逆序存储
  do {
    str[i ++] = list[tmp_value%radix];
    tmp_value /= radix;
  } while(tmp_value);
  str[i] = '\0';
  //将逆序字符串转换为正序
  char tmp;
  for (j = k; j < (i+k)/2; j++) {
    tmp = str[j];
    str[j] = str[i-j-1+k];
    str[i-j-1+k] = tmp;
  }
  return str;
}

/*
  功能:将double型数据转为字符串
  参数:value --- 输入的double浮点数
        str --- 存储转换的字符串
        eps --- 保留小数位选择,至少保留一个小数位,至多保留4个小数位
  注意:8位单片机int字节只占2个字节
*/
void sky_ftoa(double value, char *str, unsigned int eps)
{
  unsigned int integer;
  double decimal;
  char list[] = "0123456789";
  int i = 0, j, k = 0;
  //将整数及小数部分提取出来
  if (value < 0) {
    decimal = (double)(((int)value) - value);
    integer = (unsigned int)(0 - value);
    str[i ++] = '-';
    k = 1;
  } else {
    integer = (unsigned int)(value);
    decimal = (double)(value - integer);
  }
  //整数部分数据转换为字符串,逆序存储
  do {
    str[i ++] = list[integer%10];
    integer /= 10;
  } while(integer);
  str[i] = '\0';
  //将逆序字符串转换为正序
  char tmp;
  for (j = k; j < (i+k)/2; j++) {
    tmp = str[j];
    str[j] = str[i-j-1+k];
    str[i-j-1+k] = tmp;
  }
  //处理小数部分
  if (eps < 1 || eps > 4) {
    eps = 4;
  }
  
  //精度问题,防止输入1.2输出1.19等情况
  double pp = 0.1;
  for (j = 0; j <= eps; j++) {
    pp *= 0.1;
  }
  decimal += pp;
  while (eps) {
    decimal *= 10;
    eps --;
  }
  int tmp_decimal = (int)decimal;
  str[i ++] = '.';
  k = i;
  //整数部分数据转换为字符串,逆序存储
  do {
    str[i ++] = list[tmp_decimal%10];
    tmp_decimal /= 10;
  } while(tmp_decimal);
  str[i] = '\0';
  //将逆序字符串转换为正序
  for (j = k; j < (i+k)/2; j++) {
    tmp = str[j];
    str[j] = str[i-j-1+k];
    str[i-j-1+k] = tmp;
  }
  str[i] = '\0';
}


void mprintf(char * Data, ...)
{
  const char *s;
  int d;   
  char buf[16];
  uint8_t txdata;
  va_list ap;
  va_start(ap, Data);
  while ( * Data != 0 ) {				                          
    if ( * Data == 0x5c )  {									  
      switch ( *++Data ) {
        case 'r':							          
          txdata = 0x0d;
          send_uart_data(txdata);
          Data ++;
          break;
        case 'n':							          
          txdata = 0x0a;
          send_uart_data(txdata);
          Data ++;
          break;
        default:
          Data ++;
          break;
      }			 
    } else if ( * Data == '%') {									  
      switch ( *++Data ) {				
      case 's':										 
        s = va_arg(ap, const char *);
        for ( ; *s; s++) {
          send_uart_data(*((uint8_t *)s));
        }				
        Data++;				
        break;
      case 'd':			
        d = va_arg(ap, int);					
        sky_itoa(d, buf, 10);					
        for (s = buf; *s; s++) {
          send_uart_data(*((uint8_t *)s));
        }					
        Data++;				
        break;
      case 'x': {
        d = va_arg(ap, int);					
        sky_itoa(d, buf, 16);					
        for (s = buf; *s; s++) {
          send_uart_data(*((uint8_t *)s));
        }					
        Data++;			
        break;
      }
      case 'f': {
        double num = va_arg(ap, double);					
        sky_ftoa(num, buf, 4);
        for (s = buf; *s; s++) {
          send_uart_data(*((uint8_t *)s));
        }					
        Data++;			
        break;
      }
      default:
        Data++;				
        break;				
      }		 
    } else {
        send_uart_data(*((uint8_t *)Data));
        Data++;
    }
  }
}

void main(void)
{
    USART_Config();
    mprintf("STM8L start...\r\n");
    mprintf("%f %f %f\r\n", 1.2, 12.36, -1.2364568);
    mprintf("%x %x %x\r\n", 0x1035, 0x0830, 0x2018);
    mprintf("%d %d %d\r\n", 12, -12345, 2);
    mprintf("STM8L end...\r\n");
    while (1) {
        
    }
}

执行串口打印结果:

注意事项:

      如果你不需要打印浮点数,那么就把浮点数打印部分代码注释掉。由于像这种8位单片机处理浮点数,是通过编译器来软实现,故对浮点数处理可能每增加一次浮点数处理,代码就会增加几十上百个字节,我这边的整型数和浮点数处理后的编译代码大小可以看看,浮点数处理和整型数处理代码空间的差异。

没有更多推荐了,返回首页