打印
[应用相关]

在STM32上使用printf的两种方法

[复制链接]
1542|9
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
xyzjacky|  楼主 | 2019-12-26 15:50 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 xyzjacky 于 2020-1-19 21:51 编辑

单片机的开发中,很多时候我们需要看单片机的工作情况,比如看寄存器的变化,看局部变量或者全局变量,又或者程序的逻辑设计有问题,没有按预设进行某一个步骤,此时要查看程序跑到哪一个步骤里去了,等等,我们都需要查看与验证。通常我们会查用两种办法,一种是debug,一种是把要查看的信息利用某种数据接口打印出来。
debug的好处很多,不足之处也有,好处是直接看内存、看寄存器(要编译器支持,比如keil就支持STM32,51类的),用断点方式查看程序死在哪个位置,但是debug的不足之处,它有时会造成编译器崩掉等,有的编译器看局部变量只能是在打断点时才能看到。所以就要利用别的手段,就是利用某种数据接口把要查看的信息打印出来。
这里的某种数据接口是什么呢?常见的如下:
1.RS232、RS422、RS485,其实质都是UART(串口)
2.Jlink调试口
3.Ethernet(以太网接口,即网线接口)
4.WiFi
5.CAN
6.蓝牙
以上6种都是常见的数据接口。这6种接口里,第3至第6种都是属于要配合规范协议使用的接口,在它们上面使用printf会稍显麻烦(对于高手来说却是easy的),必须按规范协议来组织数据帧,自由度不高,所以单片机上常用的是串口和jlink调试口。本文就介绍如何基于串口与Jlink实现printf函数。

第一篇:基于串口的printf
既然是基于串口,当然要把串口部份的代码先调试好,即起码的收一个字节和发一个字节要没有问题才行。如何写串口的收发函数,请自行在网上搜索,这里就不跨界了。
单片机的收发数据有两种方式,一种的用中断实现,一种不用中断,printf使用的是不用中断(划重点)。不要问为什么,照着做就行了。我经常说学东西,有时不要问为什么,先照着做就行了,做多了自然就能领悟为什么。就像我们学拿筷子夹东西,不要问为什么拇指在这个位置,食指在那个位置,要论起其中的力学原理,写好多篇论文都可以。各位想想,小时候学用筷子时老是搞不利索,急得用手抓,学会了后你还会想筷子该怎么拿吗,为什么这样握吗?
下面讲具体步骤:
1.更改编译器的设置,让编译器使用Micro LIB(这一个C语言库,具体干什么的自行搜索)。以keil为例,如图:勾选Micro LIB。(下图红色框中左侧的按钮)


2.串口非中断方式的,发一个字节的函数(代码)已调试好
3.printf使用的是非中断方式的、发一个字节的函数
4.包含stdio.h这个头文件。哪一个C文件中要使用printf,就必须要包含stdio.h。如图:


5.编写代码
先看代码,如下:
1.  #ifdef __GNUC__  
2.  #define PUTCHAR_PROTOTYPE int __io_putchar(int ch)  
3.  #else  
4.  #define PUTCHAR_PROTOTYPE int fputc(int ch, FILE* f)  
5.  #endif /* __GNUC__ */  
6.     
7.  #ifdef __cplusplus  
8.  extern "C" {  
9.  #endif //__cplusplus  
10.   
11. PUTCHAR_PROTOTYPE  
12. {  
13.     RS485A_DE = DOsnt;  //发送脚使能,RS485为发送数据状态
14.     delay_us(10);  //等待发送脚的电平稳定
15.    HAL_UART_Transmit(&huart5, (uint8_t *)&ch, 1, 0xFFFF);  //调用STM32的HAL库,发送一个字节
16.     delay_us(10);  //避免数据信号震荡造成回环数据
17.     RS485A_DE = DOrec;  //发送脚除能,RS485恢复到接收数据状态
18.   return (ch);  
19. }  
20.   
21. #ifdef __cplusplus  
22. }  
23. #endif //__cplusplus  

⑴ 这段代码放在哪里。我只介绍我的经验,我的硬件是STM32的串口5 + RS485,实质是基于串口5的RS485,故这段代码放在了我的RS485.C这个文件中,因为在RS485.C文件中,我可以直接调用串口发送函数。
如果你的硬件是RS232、RS422,甚至是TTL电平(即直接使用串口),那你就自行修改。
题外话:RS232、RS422、RS485、TTL均是描述硬件的电平的,而非通常讨论的协议(如ModBus),很多人都这两个搅和在一起,一张口就是RS485协议。所谓的RS485协议也说的是OSI结构的物理层,即电路,切记。
⑵ 读者要修改的部份:紫色部份。
因为RS485的半双工通讯的,所以要把发送脚使能脚变成使能,具体的逻辑请看上面代码中的注释。如果你的电路是RS232或者TTL电平的,那这种电路是全双工的,自然就不用这一句。
关键来了,第15行的代码,它是串口发一个字节的函数。因为我用的是STM32的HAL库,所以我这里调用的是库中发送一个字节的函数:HAL_UART_Transmit()函数,如果你的代码用的是标准库,请用标准库的代码。如果是纯寄存器操作,就调用寄存器的。总之,就像第二点所说,发一个字节的函数(代码)已调试好。


   其它单片机请自行修改。

   如此准备工作就已经完成,这时,只要包含了stdio.h这个头文件的C文件中,均可以使用printf函数了,你想打印什么信息,就能打印什么,数据、文字、符号都可以。不知道怎么打印数据、文字、符号的小白,请上网搜索,或者去问问你们的计算机课老师,此处不赘述。
    然后,讲一下为何要包含stdio.h,因为printf实质是intfputc(int ch, FILE* f)函数,见代码的第4行,是在stdio.h文件中,把printf重定向了,指向了fputc()函数;而fputc()函数调用的是你串口发送一个字节(也可称字符)的函数。


使用特权

评论回复
沙发
xyzjacky|  楼主 | 2019-12-26 16:21 | 只看该作者
本帖最后由 xyzjacky 于 2020-1-19 21:56 编辑

第二篇:MDK+stm32+jlink利用swd方式进行printf输出
出处:http://www.douban.com/note/248637026/
由xyzjacky补充增加
-----------------------------------------------------------------------------------------------
作者:prife
感谢:hexlog@gmail.com
-----------------------------------------------------------------------------------------------
使用ITM机制实现调试stm32单片机,实现printf与scanf。
1. ITM简介
ITM机制是一种调试机制,是新一代调试方式,在这之前,有一种比较出名的调试方式,称为半主机(semihosting)方式。在pc上编写过C语言的人都知道,printf可以向控制台输出,scanf可以从控制台获取输入,这里的printf/scanf都是标准库函数,利用操作系统的这些函数,我们可以很方便的调试程序。在嵌入式设备上(如stm32单片机平台上)开发工具(如MDK/IAR)也都提供了标准库函,自然也提供了printf/scanf函数,那么这些函数是否可以使用呢? 问题来了,printf向哪里输出呢?并且大部分情况下,也没有键盘,又如何使用scanf实现输入呢?我们都知道,嵌入式设备一般的使用仿真器,如常见Jlink/ulink,可以实现烧录,单步,下断点,查看变量,等等。仿真器将PC机和单片机连接器来。聪明的设计者们就在考虑是否可以借助仿真器,使得单片机可以借助PC机的屏幕以及PC机的键盘实现printf的输出和scanf的按键获取。也就是说,如下的hello,world程序
1.        #include <stdio.h>  
2.        int main()  
3.        {  
4.                //硬件初始化  
5.                //....  
6.                printf("hello, world");  
7.                for(;;);  
8.        }  
这个程序烧录到单片机中后,仿真器连接接单片机与PC,开始在线调试后,那么这个程序会将"Hello, world"输出到PC机上,在开发工具(MDK/IAR等)的某个窗口中显示。这就相当于,单片机借助了PC机的显示/输入设备实现了自己的输出/输入。这种方式无疑可以方便程序开发者调试。这种机制有多种实现方式,比较著名的就是semihosting(半主机机制)和ITM机制。ITM是ARM在推出semihosting之后推出的新一代调试机制。现在我们来尝试一下这种方式调试。
2. stm32使用ITM调试
MCU:stm32f207VG
仿真器:Jlink V8
IDE:MDK4.50及以上
2.1 硬件连接
ITM机制要求使用SWD方式接口,并需要连接SWO线,一般的四线SWD方式(VCC SDCLK,SDIO,GND)是不行的。标准的20针JTAG接口是可以的,只需要在MDK里设置使用SWD接口即可。
2.2 添加重定向文件
将下面的文件保存成任意C文件,并添加到工程中。这里对这个文件简单说明一下,要知道我们的程序是在单片机上运行的,为什么printf可以输出到MDK窗口里去呢?这是因为 标准库中的printf实际上调用 fputc实现输出,所以我们需要自己编写一个fputc函数,这个函数会借助ITM(类似于USART)提供的寄存器,实现数据的发送,仿真器会收到这些数据,并发往PC机。实际上,如果你的单片机和一块LCD连接,那么你只需要重新实现fputc函数,并向LCD上输出即可,那么你调用printf时就会输出到LCD上了。这中机制,就是所谓的重定向机制。
1.        #include <stdio.h>  
2.         
3.        #define ITM_Port8(n)    (*((volatile unsigned char *)(0xE0000000+4*n)))  
4.        #define ITM_Port16(n)   (*((volatile unsigned short*)(0xE0000000+4*n)))  
5.        #define ITM_Port32(n)   (*((volatile unsigned long *)(0xE0000000+4*n)))  
6.        #define DEMCR           (*((volatile unsigned long *)(0xE000EDFC)))  
7.        #define TRCENA          0x01000000  
8.         
9.        struct __FILE { int handle; /* Add whatever you need here */ };  
10.            FILE __stdout;  
11.            FILE __stdin;  
12.              
13.        int fputc(int ch, FILE *f)   
14.        {  
15.            if (DEMCR & TRCENA)   
16.            {  
17.                while (ITM_Port32(0) == 0);  
18.                ITM_Port8(0) = ch;  
19.            }  
20.            return(ch);  
1.        }  

2.2 配置JLINK的初始化配置文件
将下面文件放置在你的工程下,并取任意名称,这里笔者取名为 STM32DBG.ini
1.        /******************************************************************************/  
2.        /* STM32DBG.INI: STM32 Debugger Initialization File                           */  
3.        /******************************************************************************/  
4.        // <<< Use Configuration Wizard in Context Menu >>>                           //   
5.        /******************************************************************************/  
6.        /* This file is part of the uVision/ARM development tools.                    */  
7.        /* Copyright (c) 2005-2007 Keil Software. All rights reserved.                */  
8.        /* This software may only be used under the terms of a valid, current,        */  
9.        /* end user licence from KEIL for a compatible version of KEIL software       */  
10.        /* development tools. Nothing else gives you the right to use this software.  */  
11.        /******************************************************************************/  
12.         
13.         
14.        FUNC void DebugSetup (void) {  
15.        // <h> Debug MCU Configuration  
16.        //   <o1.0>    DBG_SLEEP     <i> Debug Sleep Mode  
17.        //   <o1.1>    DBG_STOP      <i> Debug Stop Mode  
18.        //   <o1.2>    DBG_STANDBY   <i> Debug Standby Mode  
19.        //   <o1.5>    TRACE_IOEN    <i> Trace I/O Enable   
20.        //   <o1.6..7> TRACE_MODE    <i> Trace Mode  
21.        //             <0=> Asynchronous  
22.        //             <1=> Synchronous: TRACEDATA Size 1  
23.        //             <2=> Synchronous: TRACEDATA Size 2  
24.        //             <3=> Synchronous: TRACEDATA Size 4  
25.        //   <o1.8>    DBG_IWDG_STOP <i> Independant Watchdog Stopped when Core is halted  
26.        //   <o1.9>    DBG_WWDG_STOP <i> Window Watchdog Stopped when Core is halted  
27.        //   <o1.10>   DBG_TIM1_STOP <i> Timer 1 Stopped when Core is halted  
28.        //   <o1.11>   DBG_TIM2_STOP <i> Timer 2 Stopped when Core is halted  
29.        //   <o1.12>   DBG_TIM3_STOP <i> Timer 3 Stopped when Core is halted  
30.        //   <o1.13>   DBG_TIM4_STOP <i> Timer 4 Stopped when Core is halted  
31.        //   <o1.14>   DBG_CAN_STOP  <i> CAN Stopped when Core is halted  
32.        // </h>  
33.        _WDWORD(0xE0042004, 0x00000027);  // DBGMCU_CR  
34.        _WDWORD(0xE000ED08, 0x20000000);   // Setup Vector Table Offset Register  
35.        }  
36.         
37.        DebugSetup();                       // Debugger Setup  

这里对这个文件做简单的解释,
_WDWORD(0xE0042004, 0x00000027); // DBGMCU_CR
这一句表示想 0xE0042004地址处写入 0x000000027,这个寄存器是各个位表示的含义在注释中给出了详细的解释。 0x27即表示:
        BIT0 DBG_SLEEP
        BIT1 DBG_STOP
        BIT2 DBG_STANDBY
        BIT5 TRACE_IOEN
注意,要使用ITM机制,必须要打开BIT5。打开MDK工程,按照下图修改。

2.3 MDK中对JLINK的配置

下图中注意两点
1). 这里的CoreClock是120M,因为笔者使用的是stm32F207VG这款芯片,并且时钟配置为120M,所以这里填入120M,如果你使用stm32F10x,时钟配置成72M,那么这里需要填入72M。即需要跟实际情况保持一致。
2). 最后一定要将 0处打勾,并将其他bit位上的勾去掉,最好与此图保持一致,除CoreClock外。

2.4 烧录程序,并启动调试。可以看到,笔者在程序源码中插入了一句printf语句输出,然后按照下图,就可以看到程序的输出了。

3. 综合版本使用scanf和printf
3.1 添加retarget文件
将如下代码保存成retarget.c,然后加入到工程中。
1.        #pragma import(__use_no_semihosting_swi)  
2.         
3.        struct __FILE { int handle; /* Add whatever you need here */ };  
4.            FILE __stdout;  
5.            FILE __stdin;  
6.              
7.        int fputc(int ch, FILE *f)   
8.        {  
9.            return ITM_SendChar(ch);  
10.        }  
11.         
12.        volatile int32_t ITM_RxBuffer;  
13.        int fgetc(FILE *f)  
14.        {  
15.          while (ITM_CheckChar() != 1) __NOP();  
16.          return (ITM_ReceiveChar());  
17.        }  
18.         
19.        int ferror(FILE *f)  
20.        {  
21.            /* Your implementation of ferror */  
22.            return EOF;  
23.        }  
24.         
25.        void _ttywrch(int c)  
26.        {  
27.            fputc(c, 0);  
28.        }  
29.         
30.        int __backspace()  
31.        {  
32.            return 0;  
33.        }  
34.        void _sys_exit(int return_code)  
35.        {  
36.        label:  
37.            goto label;  /* endless loop */  
38.        }  
3.2 编译运行
编译,烧录,运行,打开Debug (printf) viewer,就可以看到输入,参看下图

这里对retarget.c文件做几点说明.
1). 上面的代码实际是在X:\Keil\ARM\Startup\Retarget.c上修改而成的,scanf依赖的函数共有两个,fgetc和__backspace都需要实现,如果缺少__backespace函数,则scanf胡无法从Debug Viewer Dialog 窗口获取输入。另外上面提供的代码只是个demo,用于演示效果,用于生产时应该处理的更完善一些。见参考文献[1]
2). 函数ITM_SendChar,ITM_CheckChar,ITM_ReceiveChar在库文件CMSIS\Include\core_cm3.h中。
3) 查看函数的符号引用关系,可以通过生成详细的map文件来查看。命令行增加 --verbose --list rtt.map选项即可生成名为rtt.map的文件。
4. ITM与RTT结合(待实现)
grissiom 写道:
忽然想到,或许可以把这个半主机做成 device,然后 rt_console_set_device("semi") 就可以直接用半主机做 finsh/rt_kprintf 了…… 不知可行不可行……
prife: ITM的接收不知道是否支持中断,目前接收字符使用是轮询方式。如果是中断才有意义。这样可以把ITM设备做成一个 rtt 的device了,让finsh跑在 Debug printf Viewer窗口上。以后只要接一个jtag/SWD口就可以调试了,不用再接串口线了。
参考文献
[1] MDK help. Indirect semihosting C library function dependencies
[2] MDK help ARM Development Tools.
         Debugger Adapter User's Guides
             J-Link/J-Trace User's Guide
         Libraries and Floating Point Support Referencee
         Libraries and Floating Point Support Guide
         Linker Reference Guide

5. 另附由xyzjacky亲测过的代码
1). 上文中所写的retarget.c,本人定义为debug.c,代码如下:
1.        /*******************************************************************************
2.         * 功能描述:调试相关设置(重定向fputc、实现printf输出)
3.         * 修改日期          版本号     修改人                                    修改内容
4.         * -----------------------------------------------------------------------
5.         * 2014/12/28             V1.0            258264176@qq.com                创建
6.        ********************************************************************************/
7.        
8.        #include "stdio.h"
9.        #include "stm32f10x.h"
10.        
11.        #pragma import(__use_no_semihosting_swi)
12.        
13.        #define ITM_Port8(n)    (*((volatile unsigned char *)(0xE0000000 + 4*n)))
14.        #define ITM_Port16(n)   (*((volatile unsigned short*)(0xE0000000 + 4*n)))
15.        #define ITM_Port32(n)   (*((volatile unsigned long *)(0xE0000000 + 4*n)))
16.        #define DEMCR           (*((volatile unsigned long *)0xE000EDFC))
17.        
18.        #define TRCENA          0x01000000
19.        
20.        struct __FILE {
21.            int handle;
22.        };
23.        
24.        FILE __stdout;
25.        FILE __stdin;
26.        
27.        /**********************************************************************
28.         * 功能描述: 实现字节输出
29.         * 参数:
30.         *      ch: 待发送的数据
31.         *      f:  句柄
32.         * 返回值:
33.         *      发送的数据
34.         ***********************************************************************/
35.        int fputc(int ch, FILE *f)
36.        {     
37.            /*USART_SendData(USART1, (u8)ch);
38.        
39.            while(RESET == USART_GetFlagStatus(USART1, USART_FLAG_TC));
40.        
41.            return ch;*/
42.            
43.                if(DEMCR & TRCENA){
44.                while(0 == ITM_Port32(0));
45.               
46.                ITM_Port8(0) = ch;
47.            }
48.            
49.            return ch;
50.        }
51.        
52.        
53.        volatile int32_t ITM_RxBuffer;
54.        int fgetc(FILE *f)
55.        {
56.                int cnt = 0;
57.          while (ITM_CheckChar() != 1)
58.                {
59.                        cnt ++;
60.                        if(cnt > 100)
61.                        {
62.                                return 0;
63.                        }
64.                        __NOP();
65.                }
66.          return (ITM_ReceiveChar());
67.        }
68.        
69.        int ferror(FILE *f)
70.        {
71.            return EOF;
72.        }
73.        
74.        void _ttywrch(int c)
75.        {
76.        
77.        }
78.        
79.        int __backspace()
80.        {
81.            return 0;
82.        }
83.        
84.        void _sys_exit(int return_code)
85.        {
86.        label:
87.            goto label;
88.        }
89.        
2). STM32DBG.ini文件内容
1.        /******************************************************************************/
2.        /* STM32DBG.INI: STM32 Debugger Initialization File */
3.        /******************************************************************************/
4.        // <<< Use Configuration Wizard in Context Menu >>> //
5.        /******************************************************************************/
6.        /* This file is part of the uVision/ARM development tools. */
7.        /* Copyright (c) 2005-2007 Keil Software. All rights reserved. */
8.        /* This software may only be used under the terms of a valid, current, */
9.        /* end user licence from KEIL for a compatible version of KEIL software */
10.        /* development tools. Nothing else gives you the right to use this software. */
11.        /******************************************************************************/
12.        FUNC void DebugSetup (void) {
13.        // <h> Debug MCU Configuration
14.        // <o1.0> DBG_SLEEP <i> Debug Sleep Mode
15.        // <o1.1> DBG_STOP <i> Debug Stop Mode
16.        // <o1.2> DBG_STANDBY <i> Debug Standby Mode
17.        // <o1.5> TRACE_IOEN <i> Trace I/O Enable
18.        // <o1.6..7> TRACE_MODE <i> Trace Mode
19.        // <0=> Asynchronous
20.        // <1=> Synchronous: TRACEDATA Size 1
21.        // <2=> Synchronous: TRACEDATA Size 2
22.        // <3=> Synchronous: TRACEDATA Size 4
23.        // <o1.8> DBG_IWDG_STOP <i> Independant Watchdog Stopped when Core is halted
24.        // <o1.9> DBG_WWDG_STOP <i> Window Watchdog Stopped when Core is halted
25.        // <o1.10> DBG_TIM1_STOP <i> Timer 1 Stopped when Core is halted
26.        // <o1.11> DBG_TIM2_STOP <i> Timer 2 Stopped when Core is halted
27.        // <o1.12> DBG_TIM3_STOP <i> Timer 3 Stopped when Core is halted
28.        // <o1.13> DBG_TIM4_STOP <i> Timer 4 Stopped when Core is halted
29.        // <o1.14> DBG_CAN_STOP <i> CAN Stopped when Core is halted
30.        // </h>
31.        _WDWORD(0xE0042004, 0x00000027); // DBGMCU_CR
32.        _WDWORD(0xE000ED08, 0x20000000); // Setup Vector Table Offset Register
33.        }
34.        DebugSetup(); // Debugger Setup
35.        
36.        /**********************************************************************************************/



STM32DBG.ini文件可直接保存在工程的任意文件夹中,本人一般保存在工程的顶层文件夹中。
附:实现printf打印信息功能的接线方式
要实现printf信息,就要按照以下方式接线(限STM32F1系列)
TDI – PA15
TMS,SWIO –PA13
TCK,SWCLK----PA14
TDO—PB3
GND----GND(不可忽略)
下面为ST-Link/V2 JTAG/SWD接口定义:
仿真器端口        连接目标板        功能
  1. TVCC          MCU电源VCC        连接STM32目标板的电源VCC
  2. TVCC          MCU电源VCC        连接STM32目标板的电源VCC
  3. TRST          GND        GROUND
  4. UART-RX          GND        GROUND
  5. TDI          TDI        连接STM32的JTAG TDI
  6. UART-TX          GND        GROUND
  7. TMS, SWIO          TMS, SWIO        连接STM32的JTAG的TMS, SWD的SW IO
  8. BOOT0          GND        GROUND
  9. TCK, SWCLK          TCK, SWCLK        连接STM32的JTAG的TCK, SWD的SW CLK
  10. SWIM          GND        GROUND
  11. NC          NC        Unused
  12. GND          GND        GROUND
  13. TDO          TDO        连接STM32的JTAG TDO
  14. SWIM-RST          GND        GROUND
  15. STM32-RESET          RESET        连接STM32目标板的RESET端口
  16. KEY          NC        GROUND
  17. NC          NC        Unused
  18. GND          GND        GROUND
  19. VDD          NC        VDD (3.3V)
  20. GND          GND        GROUND
下面是ST-Link/V2 JTAG/SWD标准的接口排列:

ST-Link/V2 JTAG/SWD指定的标准接口



使用特权

评论回复
板凳
xyzjacky|  楼主 | 2019-12-26 16:24 | 只看该作者
本帖最后由 xyzjacky 于 2019-12-26 16:26 编辑

不好意思啊,各位看官,论坛一篇帖子只能发3000个字,所以不得不分成两个。但是它又把两个部份的图片给搞到一起去了,还删除不了,真是头疼。请到我的博客里去看完整的吧,本论坛的博客和CSDN的都有。

使用特权

评论回复
地板
zhuyuye| | 2019-12-28 08:36 | 只看该作者
感谢分享

使用特权

评论回复
5
labasi| | 2020-1-17 16:07 | 只看该作者
非常感谢楼主分享

使用特权

评论回复
6
paotangsan| | 2020-1-17 16:23 | 只看该作者
非常感谢楼主分享

使用特权

评论回复
7
renzheshengui| | 2020-1-17 16:35 | 只看该作者
非常感谢楼主分享

使用特权

评论回复
8
wowu| | 2020-1-17 16:48 | 只看该作者
非常感谢楼主分享

使用特权

评论回复
9
xiaoqizi| | 2020-1-17 16:49 | 只看该作者
非常感谢楼主分享

使用特权

评论回复
10
玛尼玛尼哄| | 2020-1-17 20:56 | 只看该作者
这么多种方法。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

24

主题

112

帖子

4

粉丝