[toc]
有关 SPI 的内容在 SPI 通信协议详解,不熟悉的可以参考一下
我使用设备的是 STM32F407 + W25Q128
一、STM32 SPI 框图
![](1.png)
1、通讯引脚
这四个引脚想必大家也很熟悉了,就不过多介绍。我是用的是 SPI1
,引脚如下:
![](2.png)
SPI1
是 APB2 总线上的设备,最高通信速率达 42Mbtis/s
。
如下是 W25Q128
的引脚图:
![](3.png)
所以连接方式为:
1 2 3 4 5 6 7
| W25Q STM32 VCC --> VCC GND --> GND DO --> PA6 (MISO) DI --> PA7 (MOSI) CLK --> PA5 (SCK) CS --> PA4 (CS)
|
2、时钟控制
SCK 线的时钟信号,由波特率发生器根据“控制寄存器CR1”中的 BR[0:2] 位控制,该位是对 fpclk 时钟的分频因子, 对 fpclk 的分频结果就是 SCK 引脚的输出时钟频率,计算方法见下表:
![](4.png)
其中的 fpclk 频率是指 SPI 所在的 APB 总线频率,APB1 为 fpclk1,APB2 为 fpckl2。
通过配置“控制寄存器 CR”的 CPOL 位及 CPHA 位可以把 SPI 设置成之前分析的 4 种 SPI 模式。
3、数据控制逻辑
SPI 的 MOSI 及 MISO 都连接到数据移位寄存器上,数据移位寄存器的内容来源于接收缓冲区及发送缓冲区以及 MISO、MOSI 线。
- 当向外发送数据的时候, 数据移位寄存器以“发送缓冲区”为数据源,把数据一位一位地通过数据线发送出去;
- 当从外部接收数据的时候, 数据移位寄存器把数据线采样到的数据一位一位地存储到“接收缓冲区”中。
通过写 SPI 的“数据寄存器 DR”把数据填充到发送缓冲区中, 通过 “数据寄存器 DR”,可以获取接收缓冲区中的内容。其中数据帧长度可以通过“控制寄存器 CR1”的“DFF位”配置成 8 位及 16 位模式;配置“LSBFIRST位”可选择 MSB 先行还是 LSB 先行。
4、整体控制逻辑
整体控制逻辑负责协调整个 SPI 外设,控制逻辑的工作模式根据我们配置的“控制寄存器(CR1/CR2)”的参数而改变,基本的控制参数包括 SPI 模式、 波特率、LSB 先行、主从模式、单双向模式等等。在外设工作时,控制逻辑会根据外设的工作状态修改“状态寄存器(SR)”,我们只要读取状态寄存器相关的寄存器位, 就可以了解 SPI 的工作状态了。除此之外,控制逻辑还根据要求,负责控制产生 SPI 中断信号、DMA 请求及控制 NSS 信号线。
实际应用中,我们一般不使用 STM32 SPI 外设的标准 NSS 信号线,而是更简单地使用普通的 GPIO,软件控制它的电平输出,从而产生通讯起始和停止信号。
5、主模式收发流程及事件说明如下:
STM32 使用 SPI 外设通讯时,在通讯的不同阶段它会对“状态寄存器SR”的不同数据位写入参数,我们通过读取这些寄存器标志来了解通讯状态。
下图演示的是“主模式”流程,即 STM32 作为 SPI 通讯的主机端时的数据收发过程。
![](5.png)
- 控制 NSS 信号线, 产生起始信号(图中没有画出);
- 把要发送的数据写入到“数据寄存器 DR”中, 该数据会被存储到发送缓冲区;
- 通讯开始,SCK 时钟开始运行。MOSI 把发送缓冲区中的数据一位一位地传输出去; MISO 则把数据一位一位地存储进接收缓冲区中;
- 当发送完一帧数据的时候,“状态寄存器 SR”中的“TXE 标志位”会被置 1,表示传输完一帧,发送缓冲区已空;类似地, 当接收完一帧数据的时候,“RXNE 标志位”会被置 1,表示传输完一帧,接收缓冲区非空;
- 等待到“TXE 标志位”为 1 时,若还要继续发送数据,则再次往“数据寄存器 DR”写入数据即可;等待到“RXNE 标志位”为 1 时, 通过读取“数据寄存器 DR”可以获取接收缓冲区中的内容。
假如我们使能了 TXE 或 RXNE 中断,TXE 或 RXNE 置 1 时会产生 SPI 中断信号,进入同一个中断服务函数,到 SPI 中断服务程序后, 可通过检查寄存器位来了解是哪一个事件,再分别进行处理。也可以使用 DMA 方式来收发“数据寄存器 DR”中的数据。
有了这些基础,下面写相应的代码就轻松多了。
二、程序编写
1、SPI 初始化
我们首先实现如下两个函数:
1 2 3 4 5 6 7 8 9
| #ifndef __CTL_SPI_H #define __CTL_SPI_H
void spi_init(void); uint8_t spi_read_write_byte(uint8_t tx_data);
#endif
|
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
|
static void spi_pin_init(void) { RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; memset(&GPIO_InitStructure, 0, sizeof(GPIO_InitStructure));
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1); }
static void spi_lowlevel_init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
SPI_InitTypeDef SPI_InitStructure; memset(&SPI_InitStructure, 0, sizeof(SPI_InitStructure));
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CRCPolynomial = 7; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE); }
void spi_init(void) { spi_pin_init(); spi_lowlevel_init(); }
|
还有 SPI 的读写函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
uint8_t spi_read_write_byte(uint8_t tx_data) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { }
SPI_I2S_SendData(SPI1, tx_data);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) { }
return SPI_I2S_ReceiveData(SPI1); }
|
2、W25Q128 驱动代码
接下来需要参考手册中的时序图和指令来编写代码:W25Q128JV
下面是 FLASH常用芯片指令表:
![](6.png)
该表中的第一列为指令名,第二列为指令编码,第三至第N列的具体内容根据指令的不同而有不同的含义。
- 其中带括号的是字节参数,方向为 FLASH 向主机传输,即命令响应;不带括号的则为主机向 FLASH 传输。
- “
A0~A23
”指 FLASH 芯片内部存储器组织的地址;
- “
M0~M7
”为厂商号(MANUFACTURERID);
- “
ID0-ID15
”为 FLASH 芯片的 ID;
- “
dummy
”指该处可为任意数据;
- “
D0~D7
”为 FLASH 内部存储矩阵的内容。
如下代码,接下来,我们就将实现对应的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #ifndef __W25Q_H #define __W25Q_H
#include <stdint.h>
#define W25X_WriteEnable 0x06 #define W25X_WriteDisable 0x04 #define W25X_ReadStatusReg 0x05 #define W25X_WriteStatusReg 0x01 #define W25X_ReadData 0x03 #define W25X_FastReadData 0x0B #define W25X_FastReadDual 0x3B #define W25X_PageProgram 0x02 #define W25X_BlockErase 0xD8 #define W25X_SectorErase 0x20 #define W25X_ChipErase 0xC7 #define W25X_PowerDown 0xB9 #define W25X_ReleasePowerDown 0xAB #define W25X_DeviceID 0xAB #define W25X_ManufactDeviceID 0x90 #define W25X_JedecDeviceID 0x9F #define W25X_Dummy 0x00
typedef struct w25qxx_device_s { void (*init)(void); void (*wr)(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read); void (*rd)(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write); uint16_t type; } w25qxx_device_t;
extern w25qxx_device_t w25q32_dev;
void w25qxx_init(void); uint16_t w25qxx_readid(void); uint8_t w25qxx_readsr(void); void w25qxx_write_sr(uint8_t sr); void w25qxx_write_enable(void); void w25qxx_write_disable(void); void w25qxx_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read); void w25qxx_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write); void w25qxx_erase_chip(void); void w25qxx_erase_sector(uint32_t dst_addr); void w25qxx_powerdown(void); void w25qxx_wakeup(void);
#endif
|
除此之外,为了程序的简洁以及方便实现,定义如下的功能函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #define w25qxx_cs_high() GPIO_SetBits(GPIOA, GPIO_Pin_4) #define w25qxx_cs_low() GPIO_ResetBits(GPIOA, GPIO_Pin_4) #define w25qxx_r_w_byte(n) spi_read_write_byte(n) #define w25qxx_spi_init() spi_init() #define w25qxx_delay_us(n) bl_delay_us(n)
w25qxx_device_t w25q32_dev = { .init = w25qxx_init, .wr = w25qxx_write, .rd = w25qxx_read, .type = 0x00, };
void w25qxx_init(void) { w25qxx_spi_init(); w25q32_dev.type = w25qxx_readid(); }
|
2.1 读写厂商 ID 和设备 ID
![](7.png)
由上图可知厂商 ID 是 0xEF,设备 ID 是 0x17。
读取设备 ID 和时序图图下:
![](8.png)
该指令以 /CS 拉低开始,然后通过 DI 传输指令代码 90H
和 24 位的地址(全为 00000H
)。这之后 W25Q 的 ID(EFH
)和芯片 ID 将在时钟的下降沿以高位在前的方式传出。关于 W25Q128
的芯片和制造商 ID,在上面的图中已经列出。如果 24 位地址传输的是 00001H
,那么芯片 ID 将首先被传出,然后紧接着的是制造商 ID。这两个是连续读出来的。该指令以 /CS 拉高结束。
格式如下:
![](9.png)
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
uint16_t w25qxx_readid(void) { uint16_t id = 0; w25qxx_cs_low();
w25qxx_r_w_byte(W25X_ManufactDeviceID); w25qxx_r_w_byte(W25X_Dummy); w25qxx_r_w_byte(W25X_Dummy); w25qxx_r_w_byte(0x00);
id |= (w25qxx_r_w_byte(0xFF) << 8); id |= w25qxx_r_w_byte(0xFF);
w25qxx_cs_high();
return id; }
|
2.2 读数据
读数据指令允许从存储器读一个或连续多个字节。该指令是以 /CS 拉低开始,然后通过 DI 在时钟的上升沿来传输指令代码(03H
)和 24 位地址。当芯片接受完地址位后,相应地址处的值将会在时钟的下降沿,以==高位在前、低位在后==的方式,在 DO 上传输。如果连续的读多个字节的话,地址是==自动加 1== 的。这意味着可以一次读出整个芯片。该指令也是以 /CS 拉高来结束的。
![](10.png)
![](11.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
void w25qxx_read(uint8_t *pbuffer, uint32_t read_addr, uint16_t num_byte_to_read) { uint16_t i;
w25qxx_cs_low(); w25qxx_r_w_byte(W25X_ReadData); w25qxx_r_w_byte((uint8_t)((read_addr) >> 16)); w25qxx_r_w_byte((uint8_t)((read_addr) >> 8)); w25qxx_r_w_byte((uint8_t)read_addr); for (i = 0; i < num_byte_to_read; i++) { pbuffer[i] = w25qxx_r_w_byte(0XFF); } w25qxx_cs_high(); }
|
2.3 写使能/写禁止
![](12.png)
![](13.png)
![](14.png)
分别发送对应的两条指令即可,非常简单。
写使能指可以设置状态寄存器中的 WEL 位置 1。在页写,QUAD 页写,扇区擦除,块擦除,片擦除,写状态寄存器,擦写安全寄存器指令之前,必须先将 WEL 位置 1。写使能指令是以 /CS 拉低开始的,将 06H
通过 DI 在时钟的上升沿锁存,然后 /CS 拉高来结束指令。
写禁用指令将状态寄存器中的写启用锁存器(WEL)位重置为 0。通过低电平驱动 /CS 进入写禁用指令,将指令代码“04h
”移到 DI 引脚,然后驱动 /CS 为高电平。请注意,通电后和通电后,WEL 位会自动复位完成写状态寄存器,擦除/程序安全寄存器,页程序,扇区擦除,块擦除,芯片擦除和复位指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
void w25qxx_write_enable(void) { w25qxx_cs_low(); w25qxx_r_w_byte(W25X_WriteEnable); w25qxx_cs_high(); }
void w25qxx_write_disable(void) { w25qxx_cs_low(); w25qxx_r_w_byte(W25X_WriteDisable); w25qxx_cs_high(); }
|
2.4 读/写状态寄存器
读/写状态寄存器各有三条指令,相应内容查阅手册。
读状态寄存指令可以任何时间使用,在擦写,写状态寄存器指令周期中依然可以。这样就可以随时检查 BUSY 位,检查相应的指令周期有没有结束,芯片是不是可以接受新的指令。状态寄存器可以连续的读出来:
![](15.png)
![](16.png)
![](17.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
|
uint8_t w25qxx_readsr(void) { uint8_t byte = 0; w25qxx_cs_low(); w25qxx_r_w_byte(W25X_ReadStatusReg); byte = w25qxx_r_w_byte(0xff); w25qxx_cs_high(); return byte; }
void w25qxx_write_sr(uint8_t sr) { w25qxx_cs_low(); w25qxx_r_w_byte(W25X_WriteStatusReg); w25qxx_r_w_byte(sr); w25qxx_cs_high(); }
|
2.5 擦除扇区
扇区擦除可以擦除 4Kbit 存储空间(全为0XFF)。进行扇区擦写指令之前,必须进行写使能指令。该指令是以 /CS 拉低开始的,然后在 DI 上传输指令代码 20H
和 24 位地址。
时序图如下图。当最后字节的第 8 位进入芯片后,/CS 必须拉高。如果 /CS 没有拉高,那么扇区擦写指令将不被执行。/CS 拉高后,扇区擦写指令的内建时间为 tSE。在扇区擦写指令执行期间,读状态寄存器指令仍然可以识别,以此来进行检查 BUSY 位。当扇区擦写指令执行期间,BUSY 位为 1。当执行完后,BUSY 为 0,表明可以接受新的指令了。扇区擦写指令完成后 WEL 位自动清零。如果该指令要操作的任何–页已经被保护起来,那么该指令也将不执行。
![](18.png)
![](19.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
static void w25qxx_wait_busy(void) { while ((w25qxx_readsr() & 0x01) == 0x01) ; }
void w25qxx_erase_sector(uint32_t dst_addr) { dst_addr *= 4096; w25qxx_write_enable(); w25qxx_wait_busy(); w25qxx_cs_low(); w25qxx_r_w_byte(W25X_SectorErase); w25qxx_r_w_byte((uint8_t)((dst_addr) >> 16)); w25qxx_r_w_byte((uint8_t)((dst_addr) >> 8)); w25qxx_r_w_byte((uint8_t)dst_addr); w25qxx_cs_high(); w25qxx_wait_busy(); }
|
2.6 擦除整个芯片
芯片擦除指令将设备内的所有内存设置为全1 (FFh)的擦除状态。一个写启用指令必须在设备接受芯片擦除指令(状态)之前执行寄存器位 WEL 必须等于 1)。指令通过驱动 /CS 引脚低电平和移位启动指令代码“C7h
”或“60h
”。芯片擦除指令序列如下图所示。
芯片擦除指令将不会被执行如果任何内存区域是受块保护(CMP、SEC、TB、BP2、BP1 和 BP0)位或单个块/扇区保护锁。
![](20.png)
![](21.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
void w25qxx_erase_chip(void) { w25qxx_write_enable(); w25qxx_wait_busy(); w25qxx_cs_low(); w25qxx_r_w_byte(W25X_ChipErase); w25qxx_cs_high(); w25qxx_wait_busy(); }
|
2.7 页写
页编程指令允许 1 到 256 字节写入存储器的某一页,这一页必须是被擦除过的(也就是==只能写 0==,不能写 1,擦除时是全写为 1)。
在页编程指令之前,必须先写入写使能指令。页编程指令是以 /CS 拉低开始,然后在 DI 上传输指令代码 02H
,再接着传输 24 位的地址,接着是至少-一个字节的数据。/CS 管脚必须一直保持低。页编程指令的时序图如下图。
- 如果一次写一整页数据(256 字节),最后的地址字节应该全为 0。如果最后 8 字节地址不为 0,但是要写入的数据长度超过页剩下的长度,那么芯片会回到当前页的开始地址写。
- 写入少于 256 字节的的数据,对页内的其他数据没有任何影响。对于这种情况的唯一要求是,时钟数不能超过剩下页的长度。
- 如果一次写入多于 256 字节的数据,那么在页内会回头写,先前写的数据可能已经被覆盖。
作为擦写指令,当最后字节的第 8 位进入芯片后,/CS 必须拉高。如果 /CS 没有拉高, .那么页写指令将不被执行。/CS 拉高后,页编程指令的内建时间为 tpp。在页写指令执行期间,读状态寄存器指令仍然可以识别,以此来进行检查 BUSY 位。当页写指令执行期间,BUSY 位为了 1。当执行完后,BUSY 为 0,表明可以接受新的指令了。页写指令完成后 WEL 位自动清零。如果该指令要操作的页已经被保护起来,那么该指令也将不执行。
![](22.png)
![](23.png)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
static void w25qxx_write_page(uint8_t* pbuffer, uint32_t write_addr, uint16_t num_byte_to_write) { uint16_t i;
w25qxx_write_enable(); w25qxx_cs_low(); w25qxx_r_w_byte(W25X_PageProgram); w25qxx_r_w_byte((uint8_t)((write_addr) >> 16)); w25qxx_r_w_byte((uint8_t)((write_addr) >> 8)); w25qxx_r_w_byte((uint8_t)write_addr); for(i = 0; i < num_byte_to_write; i++) w25qxx_r_w_byte(pbuffer[i]); w25qxx_cs_high();
w25qxx_wait_busy(); }
|
接下来在这个函数的基础上,实现写函数。
2.7.1 写 SPI FLASH
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
|
static void w25qxx_write_nocheck(uint8_t* pbuffer, uint32_t write_addr, uint16_t num_byte_to_write) { uint16_t pageremain;
pageremain = 256 - write_addr % 256; if(num_byte_to_write <= pageremain) pageremain = num_byte_to_write; while(1) { w25qxx_write_page(pbuffer, write_addr, pageremain); if(num_byte_to_write == pageremain) break; else { pbuffer += pageremain; write_addr += pageremain;
num_byte_to_write -= pageremain; if(num_byte_to_write > 256) pageremain = 256; else pageremain = num_byte_to_write; } }; }
uint8_t W25QXX_BUFFER[4096];
void w25qxx_write(uint8_t *pbuffer, uint32_t write_addr, uint16_t num_byte_to_write) { uint32_t secpos; uint16_t secoff; uint16_t secremain; uint16_t i;
secpos = write_addr / 4096; secoff = write_addr % 4096; secremain = 4096 - secoff;
if (num_byte_to_write <= secremain) secremain = num_byte_to_write; while (1) { w25qxx_read(W25QXX_BUFFER, secpos * 4096, 4096); for (i = 0; i < secremain; i++) { if (W25QXX_BUFFER[secoff + i] != 0XFF) break; } if (i < secremain) { w25qxx_erase_sector(secpos); for (i = 0; i < secremain; i++) { W25QXX_BUFFER[i + secoff] = pbuffer[i]; } w25qxx_write_nocheck(W25QXX_BUFFER, secpos * 4096, 4096); } else w25qxx_write_nocheck(pbuffer, write_addr, secremain); if (num_byte_to_write == secremain) break; else { secpos++; secoff = 0;
pbuffer += secremain; write_addr += secremain; num_byte_to_write -= secremain; if (num_byte_to_write > 4096) secremain = 4096; else secremain = num_byte_to_write; } }; }
|
3、main 测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| uint8_t wr_data[128] = {0}; uint8_t rd_data[128] = {0};
int main(void) { uint8_t i = 0;
w25q32_dev.init();
printf("\r\n\r\nw25q128 id is: 0x%x\r\n", w25q32_dev.type);
printf("detact w25q128 ok!\r\n"); printf("write data !\r\n"); for (i = 0; i < 128; i++) { wr_data[i] = i; } w25q32_dev.wr(wr_data, 0, 128);
w25q32_dev.rd(rd_data, 0, 128); printf("\r\nread data is :\r\n"); for (i = 0; i < 128; i++) { printf("%d, ", rd_data[i]); }
return 0; }
|
测试结果如下:
![](24.png)