2018-09-15 11:20:14 limubai__ 阅读数 885
  • 串口通信和RS485-第1季第13部分

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

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

基本信息:

系统:Windows10

编译器:MPLAB X IDE V3.26 + XC8 V1.45

单片机:PIC18F66K22

下载器:PICKit3


一、串口初始化

/*

函数名:void USART1_Init( void )

功能:使用的是串口1,串口初始化

*/

void USART1_Init( void )

{
    TRISC6 = 1;
    TRISC7 = 1;        // 输入模式,作复用功能

    TX91 = 0;            // 8位发送
    TXEN1 = 1;
    SYNC1 = 0;            // 异步模式
    BRGH1 = 1;
    RX91 = 0;
    CREN1 = 1;            
    
    SPEN1 = 1;            // 使能串口
    SPBRG1 = 207;        // 波特率9600
}

二、printf()函数重定向

printf函数可以完成两件事:它基于你指定的格式字符串和占位符格式化文本,并将改格式化文本发送(打印)到目标(或流)然后它会调用一个putch函数来发送格式化文本的每一个字节。

/*
函数名:void putch( char ch )
功能:重定向printf函数
*/

void putch( char ch )
{
    while( !TX1IF )
        continue;
    TXREG1 = ch;
}

                                                                                                               END

2017-12-01 01:57:06 qq_32108893 阅读数 1199
  • 串口通信和RS485-第1季第13部分

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

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

以前在单片机上使用OLED或者做串口通信都是写第层驱动函数,然后使用底层函数显示/发送数据,但是这样的话使用起来始终感觉不是很方便。所以前两天开始琢磨有没有更方便的方式来显示数据,最好能够像printf一样输出格式化字符。到网上一查还真有,但是大多数说的都做串口的重定向,而且说的也不是很详细,但是经过两天的研究也大概了解了怎么去做printf的重定向。

首先要了解什么是重定向,简单来说printf的重定向就是把原本要输入到控制台的内容输出到其他的地方去,这个其他的地方可以是串口,也可以是液晶屏,甚至是重定向到jlink上输出到电脑(使用RTT)。

我的目最初的目的是想让液晶屏显示数据能用上printf所以就先研究了OLED上的printf重定向,在网上查资料发现printf的重定向实际上是有两种方式,一种是通过重新定义一个

int fputc(int ch, FILE *f){
        //里面是要重定向的设备显示/发送一个字节的代码
    };

具体我是这么去实现的:

//printf 函数重定向到OLED显示
int fputc(int ch, FILE *f)
{      
    static uint8_t x = 0, y = 0;
    if(ch == '\n')    //换行 
    { 
        x = 0; 
        y += 1; 
        return ch; 
    }
    if(x + 6 > 128)  //宽不能超过128
    { 
        x = 0;           //x置零 
        y += 1;         //y移至下一行 
    } 
    if(y + 1 > 7)  //行不超过7
    { 
        y=0;
        OLED_Fill(0x00); //直接清屏 重头开始 
        x = 0;
    }

    OLED_P6x8char(x, y, (uint8_t)ch); //打印字符ch 
    x += 6;                   //跳转到下一个位置, 是否越界有上面函数判断
    return ch; 
}

这样的一个函数。

另一种方式就是自己去写一个printf函数,我是这么写的:

signed int OLED_printf(uint8_t x,uint8_t y,const char *pFormat, ...){

    char pStr[25] = {'\0'}; 
    va_list ap;
    signed int result;

    // Forward call to vprintf
    va_start(ap, pFormat);
    result = vsprintf((char *)pStr, pFormat, ap);
    va_end(ap);

    OLED_P6x8Str(x,y,(const uint8_t *)pStr);

    return result;
}

实际上基本是照抄printf();里面的内容然后稍加改动来实现的。

上面所说的两种方法我都试过,都能实现printf的重定向,其中网上看到的大多数串口重定向都是通过第一种方式来实现。具体原理我先说第二种的实现再回头看第一种的就很简单。

    va_start(ap, pFormat);
    result = vsprintf((char *)pStr, pFormat, ap);
    va_end(ap);

第二种方法中,以上这三句话就做了一件事情,那就是把传进来的一个或多个参数全部整合成一个字符串,放到pStr这个字符串数组里。这样我们就得到了一整个要显示的字符串,再通过OLED的一个显示字符串的函数把这些字符串都显示出来就OK了,顺带着这种方法还可以实现想在OLED那个地方输出字符串都可以毕竟调用的就是OLED的字符串显示函数本来就要有一个要显示的坐标。
printf实现的具体原理,已经这三句话里面到底干了什么可以参考这篇帖子:
[printf函数实现原理]http://blog.csdn.net/yskcg/article/details/6073067

OLED_P6x8char(x, y, (uint8_t)ch); //打印字符ch 

再回头去看第一种方法的实现方式,在上面的那一连串的几个if都是防止OLED显示溢出,真正核心的只有上面这一个函数就是OLED输出一个字符。实际上你大概也能猜的到了,printf的底层下面实际上就是调用了int fputc(int ch, FILE *f)这个函数,我们重写了这个函数,让它变成在OLED上显示当然就完成了printf的重定向了但是这个方式在OLED上有一个缺陷,就是printf()本身是输出到控制台上的,所以不能带有坐标参数,但是OLED上右必须指定输出位置,所以程序中就只能让OLED从上到下输出,像在控制台上一样,输出满屏幕之后清屏然后再接着输出。

网上之所以串口重定向用的基本上都是第一种方法也是因为串口没必要指定输出位置重写fputc函数写起来也方便。

解决了重定向的问题了之后,我本以为已经可以随心所欲的使用printf了直到我测试了浮点数的输出,发现输出一直不对,然后去跟踪printf的底层(好像也就IAR能看到printf的实现MDK是看不到stdio.c的内容的),发现了IAR的标准输入输出库坑爹的一个地方:它本来就不支持浮点数的输出,这是它里面判断格式化字符的代码:他原本的代码里根本就没有判断%f这个格式符,坑爹啊!!

// Parse type
switch (*pFormat) {
case 'd': 
case 'i': num = PutSignedInt(pStr, fill, width, va_arg(ap, signed int)); break;
case 'u': num = PutUnsignedInt(pStr, fill, width, va_arg(ap, unsigned int)); break;
case 'x': num = PutHexa(pStr, fill, width, 0, va_arg(ap, unsigned int)); break;
case 'X': num = PutHexa(pStr, fill, width, 1, va_arg(ap, unsigned int)); break;
case 's': num = PutString(pStr, va_arg(ap, char *)); break;
case 'c': num = PutChar(pStr, va_arg(ap, unsigned int)); break;
//%f是自己添加的
case 'f': num = PutSignedInt(pStr, fill, width, va_arg(ap, signed int)); break;
default:
   return EOF;
}

一开始我还自己写了一个浮点数转字符串的函数,也能用就是每次printf都得用%s来输出转换的数据:
就像我想输出一个3.14就得OLED_printf(0,0,”%s \n”,float2str(3.14));始终觉得不爽

uint8_t* float2str(double num,int n){

    int i = 0;
    static uint8_t num_str[13] = {'\0'}; 
    uint8_t len = 0;
    uint8_t n_max = 0; //小数点最大位数

    for(i = 0;i<13;i++) //字符串清空
      num_str[i] = '\0';
    //确定符号
    if(num > 0){

      num_str[0] = '+';
    }
    else if(num < 0){

      num_str[0] = '-';
      num = -num;
    }

    //确定整数部分长度
    if((int)(num/100000) != 0){ //6位数
      len = 7;
      n_max = 4;
    }
    else if((int)(num/10000) != 0){ //5位数 
        len = 6;
        n_max = 5;
    }
    else if((int)(num/1000) != 0){ //4位数   
        len = 5;
        n_max = 6;
    }
    else if((int)(num/100) != 0){  //3位数
        len = 4;
        n_max = 7;
    }
    else if((int)(num/10) != 0){ //2位数
        len = 3;
        n_max = 8;
    }
    else{ //1位数
        len = 2;
        n_max = 9;
    }

    //小数点限幅
    if(n > n_max)
        n = n_max;
    len+=n; //确定长度

    //变成整数
    i = n;
    while(i--)
        num = (num*10);

    //inum = num;

    //转换小数
    while(n--){ 

        num_str[len--] = (int)num%10+0x30;
        num = num/10;
    }
    num_str[len] = '.';

    //转换整数
    while(len--){ 

        num_str[len] = (int)num%10+0x30;
        num = num/10;
        if(len == 1)
          break;
    }
    return num_str;
}

但是我又看了它整型输出格式化字符串的函数PutSignedInt();感觉好像用在浮点数上也没毛病,所以就自己加了一个%f的判断,把%d下面的代码抄了一遍放到%f下,神奇的是这居然真的就好使了!这个输出浮点数默认是保留6位小数,所以要想保留2位小数的话得用%.2f。总之输出浮点数也没问题了。

我个人感觉是自己重新写printf会比较灵活,如果用重写fputc的方法的话,假如串口拿来重写了,OLED就不能再定义这么一个函数了,所以只能有一个设备能用fputc的方式来实现printf重定向,但是自己实现printf就不一样了,改一个名字就是一种外设的printf,比如OLED_printf,UART_printf啊什么的个人感觉比较灵活。

注意:单片机使用printf的时候一定要保证堆栈是充足的!因为printf在实现的时候是不检测内存溢出的,所以堆栈不够大的时候就会出现内存溢出,影响程序中其他变量!


printf用不了浮点输出的根本原因是因为IAR没有设置对,在Option->General Option->Library Option选项卡里面有关于Printf formatter 的选项,选到FULL就可以使用printf的所有功能,选到Small就不支持浮点输出,当然占用的内存也会小一些,看需求选取即可这里写图片描述


关于printf重定向的问题其实还没完,弄完OLED的重定向之后我又在琢磨,jlink是不是也能重定向。又是到网上一查发现还真能!而且SEGGER已经写好了代码了,直接调用就行,叫做RTT(real time terminal),jlink软件V4.9以上支持,有相应的RTT跟上位机差不多的软件有跟编译器配合用的也有不需要配合用的。

RTT其实就是跟串口重定向是一样的,它把数据重定向到jlink上输出到电脑。而且RTT还有一个优点就是不占用CPU时间,用了之后jlink自己去访问内存发送数据,不需要CPU进行参与,当然调用发送函数花的时间还是得有的。RTT从SEGGER官网下下来有4个文件,分别是这4个,都加到工程里,然后包含SEGGER_RTT.h就可以用了。
RTT的4个文件

但是坑爹的事情又出现了,RTT双不支持浮点数的数据,又是跟踪进去一看,又是底层没有判断%f,而且它没有调用标准输入输出库是自己实现printf的。其实我跟踪进去看,发现其实它和标准库的实现套路基本上是一样的,都是先把那些参数全部都给弄成一个一个字符串然后输出出来,但是我用改IAR标准库的方式去改RTT的printf发现不好使,看来它具体的实现跟标准库还是有差距的。
SEGGER它自己的实现有问题(其实不是它有问题而是它不支持浮点输出),但是我想要能用%f输出浮点数,那就只能自己写一个了呀,方法跟上面所提的是一样的。要重定向的关键是需要一个可以发送字符串的函数,这个函数SEGGER已经封装好了,如下所示:

int SEGGER_RTT_Write(unsigned BufferIndex, const char* pBuffer, unsigned NumBytes)

所以在 SEGGER_RTT_printf.c 里,把它自己实现的printf和其他相关函数全部注释掉再引入标准输入输出库stdio.h 和 stdarg.h(va_start 和 va_end使用)最后自己写printf函数如下:

signed int RTT_printf(unsigned BufferIndex,const char *pFormat, ...){

    char pStr[50] = {'\0'}; 
    va_list ap;
    signed int result;

    // Forward call to vprintf
    va_start(ap, pFormat);
    result = vsprintf((char *)pStr, pFormat, ap);
    va_end(ap);

    SEGGER_RTT_WriteString(BufferIndex,(char *)pStr);

    return result;
}

其实就是OLED 显示的printf吧显示字符串改成了RTT的发送字符串函数,然后把坐标变成了发送通道,测试之后问题完美解决,本质其实就是用标准库得到了正确的字符串所以输出正确,SEGGER自己的实现得不到,就显示出错。
这里写图片描述

重定向到OLED 的现象其实也是差不多的,我就不多贴图了。


我之前用的SEGGER_RTT不是在官网下的(宿舍网速太渣,下载速度5Kb/s下不下来),可能版本比较旧是自己做printf实现的数据输出,我今天下了最新的RTT发现它自己都改成了用标准库做重定向,具体如下:

int printf(const char *fmt,...) {

  char buffer[128];
  va_list args;
  va_start (args, fmt);
  int n = vsnprintf(buffer, sizeof(buffer), fmt, args);
  SEGGER_RTT_Write(0, buffer, n);
  va_end(args);
  return n;
}

这直接是从官方RTT文件里面复制出来的,有没有觉得有有那么一些似曾相识的感觉。。。。。。
官方给的代码还默认是通道0的呢,实际上可以有15个通道,总感觉我那么些还比较好一点


2017-12-14
经过实测,Jlink RTT Viewer 好像只能接受通道0的数据,其实只用一个通过就够了

2018-03-19 15:27:38 qq_22329595 阅读数 1087
  • 串口通信和RS485-第1季第13部分

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

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

搜索fputc函数,将fputc函数中的串口配置修改成所应用的串口,然后就可以使用printf函数打印了


2019-01-31 12:30:33 wuyuzun 阅读数 876
  • 串口通信和RS485-第1季第13部分

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

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

前言

  1. 本博文基于STM32F103ZET6和MDK5.2.6和库函数V3.5.0开发;
  2. 本博文采用七星虫德飞莱开发板,USB-TTL电路,USART1和串口调试助手;
  3. 如有不足,多指教;

串口通信作为拓展单片机功能的一个外设,其本身还有一个常用的功能就是用于调试使用,通过对一个值的输出从而观察所要的值是否正确,比较形象,但是自己在写串口的时候写出来的程序不能像当初VC++里的C语言一样利用printf()函数还输出汉字,而是一堆乱码的东西或者16进制数字,看着很难受,于是就决定把这个东西的原理给搞懂;串口的配置就不多说了,主要是针对STM32来说,printf()函数该怎么写;

printf()的重定向(三个步骤)

步骤一:
重定向: 简单来说就是用户写了一个跟C标准库一模一样的函数,在进行工程编译时,连接器检查到用户自己编写的有某个和C库函数同名的用户写的函数,就会优先采用用户编写的函数,这就叫重定向; 而且这里我们为了实现重定向printf()函数,我们重写了fputc(int ch,FILE *f)这个C标准库函数,因为printf()在C标准库函数中实质上是个宏定义,最终调用的是fputf(int ch,FILE *f)这个函数;

//重定向C库函数printf()到串口,重定向后可使用printf();
int fputc(int ch,FILE *f)
{
	USART_SendData(USART1,(uint8_t)ch);
	while(!(USART_GetFlagStatus(USART1,USART_FLAG_TC)));
	return ch;
}

printf()对应的底层函数接口在"stdio.h"中的第673行
在这里插入图片描述
注:

  1. ch为要发送的8位数据;
  2. 若使用C标准输出库函数,需要在main.c中把stdio.h头文件包含进来,还要在魔法棒中勾选一个“Use MicroLIB(使用微库)”,这个微库是MDK为嵌入式应用量身定做的C库,在编程时,要先有库,才能重定向;如下图配置;
    步骤二
    在这里插入图片描述
    步骤三
    在需要用到printf()函数的文件中加上头文件 stdio.h 文件;如下列工程;
#include "stm32f10x.h"
#include "usart.h"
#include "stdio.h"   //标准输入输出头文件;
#include "systick.h"

int main()
{
	systick_Init();
	USART1_Init(115200);
	while(1)
	{
		printf("无语尊wuyuzun\n");
		delay_ms(100);
	}	
}

完成上面这两个操作后,就可以通过串口助手软件看输出了:
在这里插入图片描述

扩展

在对STM编程时,除了可以对printf进行重定向之外,还可以对scanf重定向;
scanf()重定向函数如下:

int fgetc(FILE *stream)
{
    while(!(USART1->SR & (1 << 5)));//等待数据接收完成
    return USART1->DR;
}

scanf()对应的底层函数接口在"stdio.h"中的第649行:
在这里插入图片描述

同理,还可以拓展C库中其他函数;

2014-08-06 02:16:16 Leytton 阅读数 1614
  • 串口通信和RS485-第1季第13部分

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

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

【转载请注明出处:http://blog.csdn.net/leytton/article/details/38393967

视频教程

http://v.pps.tv/play_38CV0A.html?sort=desc


此处是为了在串口中断通信[参见另一篇文章]中添加printf重定向功能

1、添加头文件  #include <stdio.h>

2、工程“Target" -->勾选 "Use MicroLIB"

3、重定义fputc函数

int fputc(int ch, FILE *f)
{
Uart1_PutChar((u8)ch);  //此处为自定义函数,参见串口中断通信,请勿盲目复制
return (ch);
}

经过上述配置后即可在项目中使用printf("Hello~");等来发送字符串了

(printf("<格式化字符串>", <参量表>) 与C语言使用一样)






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