为了控制或读取外挂模块,stm32需要与外挂模块进行通信,来扩展硬件系统。通信双方需要遵守通信协议,也就是双方需要按照协议规则进行数据收发,不同的外挂模块会有不同的通信协议。
下面介绍一下引脚的全称:USART:TX(Transmit Exchange)数据发送脚、RX(Receive Exchange)数据接收脚。
- IIC:SCL(Serial Clock)时钟线、SDA(Serial Data)数据线。
- SPI:MOSI(Master Output Slave Input)主机输出数据脚、MISO(Master Input Slave Output)主机输入数据脚、CS(Chip Select)片选
- USB:DP(Data Postive)差分线正、DM(Data Minus)差分线负
注:上述协议中,单端电平都需要共地。
注:使用差分信号可以抑制共模噪声,可以极大的提高信号的抗干扰特性,所以一般差分信号的传输速度和传输距离都非常高
?
下面介绍一些串口引脚的注意事项:
- TX与RX:简单双向串口通信有两根通信线(发送端TX和接收端RX),要交叉连接。不过,若仅单向的数据传输,可以只接一根通信线。
- GND:一定要共地。由于TX和RX的高低电平都是相对于GND来说的,所以GND严格来说也算是通信线。
- VCC:相同的电平才能通信,如果两设备都有单独的供电,VCC就可以不接在一起。但如果某个模块没有供电,就需要连接VCC,注意供电电压要按照模块要求来,必要时需要添加电压转换电路。
串口协议的软件部分:
?
?
下面介绍串口的参数:
- 波特率:串口通信的速率(bit/s),也就是通信双方所约定的通信速率(异步通信)。
- 空闲状态:固定为高电平。
- 起始位:固定为低电平,标志一个数据帧的开始。
- 数据位:低位先行,数据帧的有效载荷,1为高电平,0为低电平。
- 校验位(选填):用于数据验证,根据数据位计算得来。
- 停止位:固定为高电平,用于表示数据帧的间隔,同时也可以使得通信线回归到空闲状态。可以配置停止位是1位/2位。
?
USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步收发器 是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据 自动生成数据帧时序,从TX引脚发送出去,也可 自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。USART中的“S”表示同步,只支持时钟输出,不支持时钟输入,是为了兼容别的协议或特殊用途而设计的,并不支持两个USART之间进行同步通信,所以这个功能几乎不会用到,一般更常使用的是UART同步异步收发器。下面是一些参数:
?
- 自带波特率发生器,最高达4.5Mbits/s,常用9600/115200。
- 可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)。
- 可选校验位:无校验(常用)/奇校验/偶校验。
- 支持同步模式(一般不用)、硬件流控制(指示从设备准备好接收的信号,一般不用)、DMA、智能卡、IrDA(手机红外通信,但并不是红外遥控,目前很少见)、LIN(局域网的通信协议)。
- STM32F103C8T6 USART资源:USART1(APB2)、USART2(APB1)、USART3(APB1)。
?
?
上图给出USART最主要、最基本的结构:
- 波特率发生器:用于产生约定的通信速率。时钟来源是PCLK2/PCLK1,经过波特率发生器分频后,产生的时钟通向发送控制器、接收控制器。
- 发送控制器、接收控制器:用于控制发送移位、接收移位。
- GPIO:发送端配置成复用推挽输出、接收端配置成上拉输入。
- 标志位:TXE置位时写入数据、RXNE置位时接收数据。
- 开关控制:用于开启整个USART外设。
?
小细节:计算分频系数DIV
?
?细节2:
USB转串口模块
?
主要关注的是该模块的供电情况。
- USB插座:直接插在电脑USB端口上,注意整个模块的供电来自于USB的VCC+5V。
- CON6插针座:
- 引脚2、引脚3:用于连接到stm32上进行串口通信。
- 引脚5【CH340_VCC】:通过跳线帽可以选择 接入+3.3V(stm32) 或者+5V。CH340芯片的供电引脚,同时决定了TTL,所以也就是串口通信的TTL电平。神奇的是,即使不接跳线帽CH340也可以正常工作,TTL为3.3V,但是显然接上电路以后更加稳定。
- 通信和供电的选择:CON6插针座选择引脚4/6进行通信后,剩下的引脚可以用于给从设备供电,但是剩下的这个脚显然与TTL电平不匹配。此时需要注意 优先保证供电电平的正确,通信TTL电平不一致问题不大。当然,若从设备自己有电源,那么就不存在这个问题了。
- TXD指示灯、RXD指示灯:若相应总线上有数据传输,那么指示灯就会闪烁。
在软件代码中定义要发送的信息,然后通过串口发送到电脑端,使用“串口助手”小工具查看。要求依次发送单字节数据、数组、字符串、数据的每一位。
注:串口助手可以切换“文本模式”/“HEX模式”。
注:数字和字符的对应关系可以参考ASCII码表。
main.c?
#include "stm32f10x.h" // Device header
#include "SerialPort.h"
int main(void){
uint8_t send_byte = 0x42;
uint8_t send_array[6] = {0x30,0x31,0x32,0x33,0x34,0x35};
//串口初始化
SerialPort_Init();
//发送单个字节
SerialPort_SendByte('A');//可以直接发送字符
SerialPort_SendByte(send_byte);
SerialPort_SendByte('\r');
SerialPort_SendByte('\n');
//发送数组
SerialPort_SendArray(send_array,6);
SerialPort_SendByte('\r');
SerialPort_SendByte('\n');
//发送字符串
SerialPort_SendString("Hello World!\r\n");
//发送数字的每一位
SerialPort_SendNum(65535, 5);
SerialPort_SendString("\r\n");
while(1){
// //循环发送数字
// SerialPort_SendByte(send_byte);
// OLED_ShowHexNum(1,9,send_byte,2);
// send_byte++;
// Delay_ms(1000);
};
}
??SerialPort.h
#ifndef __SERIALPORT_H
#define __SERIALPORT_H
void SerialPort_Init(void);
void SerialPort_SendByte(uint8_t send_byte);
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array);
void SerialPort_SendString(char *send_string);
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len);
#endif
SerialPort.c
#include "stm32f10x.h" // Device header
//串口初始化-USART1
void SerialPort_Init(void){
//1.开启RCC外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//2.初始化GPIO-PA9
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//3.初始化USART结构体
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
//4.配置中断,开启NVIC(接收数据使用)
//5.开启外设
USART_Cmd(USART1, ENABLE);
}
//串口发送1字节数据
void SerialPort_SendByte(uint8_t send_byte){
//向发送数据寄存器TDR中写入数据
USART_SendData(USART1, send_byte);
//确认数据被转移到发送移位寄存器(等待标志位TXE置位)
while(USART_GetFlagStatus(USART1, USART_FLAG_TXE)==RESET);
}
//发送一个数组
void SerialPort_SendArray(uint8_t *send_array, uint16_t size_array){
uint8_t i=0;
for(i=0;i<size_array;i++){
SerialPort_SendByte(send_array[i]);
}
}
//发送一个字符串
void SerialPort_SendString(char *send_string){
uint8_t i=0;
for(i=0; send_string[i]!='\0'; i++){
SerialPort_SendByte(send_string[i]);
}
}
//非外部调用函数-幂次函数
uint32_t SerialPort_Pow(uint32_t X, uint32_t Y){
uint32_t result = 1;
while(Y--){
result *= X;
}
return result;
}
//发送数字的每一位-先发高位
void SerialPort_SendNum(uint32_t send_num, uint16_t send_len){
uint16_t i;
for(i=0;i<send_len;i++){
SerialPort_SendByte((send_num/SerialPort_Pow(10,send_len-i-1))%10+'0');
}
}
将C语言自带函数printf
进行封装,默认成将需要打印的数据发送到串口,进而可以显示在电脑端串口助手上。
法一:
#include <stdio.h>
//对printf函数重定向-将fputc函数原型重定向到串口
//注:ptintf函数本质上就是循环调用fputc,将字符一个一个输出
int fputc(int ch, FILE *f){
SerialPort_SendByte(ch);
return ch;
}
在?SerialPort.h?模块中添加下列代码
#include <stdio.h>
于是就可以在?main.c?中调用printf
函数,将数据输出到串口了。
//使用重定向的printf函数
printf("%d\r\n",666);
法2:
若多个串口都想使用printf函数,那么就可以使用sprintf函数。sprintf函数可以将格式化字符输出到一个字符串里,然后再调用相应的“串口发送字符串”函数发送这个字符串,整个过程不涉及重定向,于是就实现了所有USART外设都可以打印信息到串口了。所以下面可以直接在 main.c 中定义:
char String[100];//定义一个足够长的字符串数组
sprintf(String, "Num=%d\r\n", 666);//将格式化字符串存储在String中
SerialPort_SendString(String);//串口发送字符串
程序整体思路:
- 查询。主函数不断查询RXNE标志位,但是会占用很多的CPU资源,所以不推荐。
- 中断。推荐方法,下面的演示也是基于此方法。
main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "SerialPort.h"
int main(void){
uint8_t Rx_byte = 0;//串口接收的单比特数据
//设置中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED初始化
OLED_Init();
OLED_ShowString(1,1,"Rx_byte:");
//串口初始化
SerialPort_Init();
while(1){
if(SerialPort_GetRxFlag()==1){
Rx_byte = SerialPort_GetRxData();
OLED_ShowHexNum(1,9,Rx_byte,2);
SerialPort_SendByte(Rx_byte);
}
};
}
?SerialPort.c
uint8_t SerialPort_RxData = 0;
uint8_t SerialPort_RxFlag = 0;
//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){
if(SerialPort_RxFlag==1){
SerialPort_RxFlag = 0;
return 1;
}else{
return 0;
}
}
//获取接收的数据
uint8_t SerialPort_GetRxData(void){
return SerialPort_RxData;
}
//USART1_RXNE中断函数
void USART1_IRQHandler(void){
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
SerialPort_RxFlag = 1;
SerialPort_RxData = USART_ReceiveData(USART1);
//读操作可以自动清零标志位,但加上也没事
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//不要忘了将前两个函数在头文件中声明
数据包的作用是将一个个单独的数据打包,方便进行多字节的数据通信。因为实际应用中,经常需要进行数据打包。比如陀螺仪传感器需要将数据发送到stm32,其中包括X轴、Y轴、Z轴三个字节,循环不断的发送;若采用一个一个进行发送的方式,接收方就有可能分不清对应的顺序,进而出现数据错位现象。此时,若能将同一批数据进行分割和打包,就可以方便接收方识别。
1:数据包格式定义:
HEX数据包
若载荷数据与包头、包尾一样怎么办呢?有三种解决思路:?
- 限制载荷数据的范围。使其不会与包头、包尾重复。
- 尽量使用固定长度的数据包。只要数据长度固定,那么就可以通过包头、包尾定位数据。
- 增加包头包尾的数量,使其尽量呈现出载荷数据不会出现的状态。
注:包尾可以去除。但是这样会使得载荷数据和包头重复的问题更加严重。
若想发送16位整型数据、32位整型数据、float、double、结构体等,只需使用?uint8_t
型指针?指向这些数据,就可以进行发送(将各种数据转换成字节流)。?文本数据包
?文本数据包中,每个数据都经过了一层编码和译码。
由于包头包尾非常容易唯一确定,文本数据包基本不用担心载荷数据和包头包尾重复的问题。
HEX数据包:
优点:传输最直接,解析数据非常简单,比较适合一些模块发送最原始的数据。如使用串口通信的陀螺仪、温湿度传感器。
缺点:灵活性不足,载荷容易和包头包尾重复。
文本数据包:
优点:数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合。如蓝牙模块常用的AT指令、CNC和3D打印机常用的G代码,都是文本数据包的格式。
缺点:解析效率低。
2:数据包的收发流程
?数据包发送是非常简单的,直接发就完事儿了。但是接收数据包的过程比较复杂,这是就要考虑使用状态机。
图9-19 接收HEX数据包-状态机
?自定义数据包格式,使用串口完成指定格式的数据包收发,并将收发结果显示在OLED上。另外按键的功能是将发送的当前存储的发送数据全部加1再发送出去。
- 数据包头:0xFF。
- 载荷数据:固定数据段长度为4个字节。
- 数据包尾:0xFE。
?main.c
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "SerialPort.h"
#include "Key.h"
int main(void){
//存储串口接收的HEX数据包
uint8_t *Rx_Packet = SerialPort_GetRxPacket();
//存储串口发送的HEX数据包
uint8_t Tx_Packet[4] = {0x01,0x02,0x03,0x04};
//设置中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED初始化
OLED_Init();
OLED_ShowString(1,1,"Rx_Packet:");
OLED_ShowString(3,1,"Tx_Packet:");
//串口初始化
SerialPort_Init();
//按键初始化
Key_Init();
while(1){
//显示接收到的数据
if(SerialPort_GetRxFlag()==1){
OLED_ShowHexNum(2,1,*Rx_Packet,2);
OLED_ShowHexNum(2,4,*(Rx_Packet+1),2);
OLED_ShowHexNum(2,7,*(Rx_Packet+2),2);
OLED_ShowHexNum(2,10,*(Rx_Packet+3),2);
}
//检测按键,发送数据包到电脑
if(Key_GetNum()==1){
Tx_Packet[0]++;
Tx_Packet[1]++;
Tx_Packet[2]++;
Tx_Packet[3]++;
SerialPort_SendPacket(Tx_Packet);
OLED_ShowHexNum(4,1,Tx_Packet[0],2);
OLED_ShowHexNum(4,4,Tx_Packet[1],2);
OLED_ShowHexNum(4,7,Tx_Packet[2],2);
OLED_ShowHexNum(4,10,Tx_Packet[3],2);
}
};
}
?SerialPort.c新增函数
uint8_t SerialPort_RxPacket[4];
uint8_t SerialPort_RxPacketFlag = 0;
//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){
if(SerialPort_RxPacketFlag==1){
SerialPort_RxPacketFlag = 0;
return 1;
}else{
return 0;
}
}
//获取接收的HEX数据包
uint8_t* SerialPort_GetRxPacket(void){
return SerialPort_RxPacket;
}
//发送HEX数据包
void SerialPort_SendPacket(uint8_t *send_array){
SerialPort_SendByte(0xFF);
SerialPort_SendArray(send_array, 4);
SerialPort_SendByte(0xFE);
}
//USART1_RXNE中断函数
void USART1_IRQHandler(void){
uint8_t rec_byte;
static uint8_t rx_state;
static uint8_t rx_index;
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
rec_byte = USART_ReceiveData(USART1);
//利用状态机,接收HEX数据包
if(rx_state==0){
if(rec_byte==0xFF){
rx_index = 0;
rx_state = 1;
}
}else if(rx_state==1){
SerialPort_RxPacket[rx_index] = rec_byte;
rx_index++;
if(rx_index>=4){
rx_state = 2;
}
}else if(rx_state==2){
if(rec_byte==0xFE){
SerialPort_RxPacketFlag = 1;
rx_state = 0;
}
}
//读操作可以自动清零标志位,但加上也没事
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//除了中断函数,其余函数还要在头文件SerialPort.h中声明
数据混叠。若电脑端连续发送数据包,而stm32处理不及时,会导致数据错位。但是一般像传感器模块等的数据都具有连续性,所以就算数据错位也没关系。
发送数据不匹配。注意发送字节数据一定要写成0x11的形式,而不是直接写一个11进行发送。
收发数据没反应。注意一定要在最开始声明的地方赋初值,否则有可能读不出数据。当然,还有一种可能是串口连接不稳定,可以重新拔插一下串口。
?
?使用式的文本数据包,来控制单片机点亮或熄灭LED灯,单片机完成指令后再将接收的状态回传到电脑。电脑端发送指定格
- 数据包头:@。
- 数据包:有效指令为"@LED_ON\r\n"、“@LED_OFF\r\n”。(不定字长)
- 数据包尾:\r\n。
?
main.c?】
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "SerialPort.h"
#include "LED.h"
#include <string.h>
int main(void){
//存储串口接收的HEX数据包
char *Rx_Packet = SerialPort_GetRxPacket();
//设置中断分组
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//OLED初始化
OLED_Init();
OLED_ShowString(1,1,"Rx_Packet:");
OLED_ShowString(3,1,"Tx_Packet:");
//串口初始化
SerialPort_Init();
//LED初始化
LED_Init();
while(1){
//对接收到的文本进行判断
if(SerialPort_GetRxFlag()==1){
//OLED显示接收到的文本
OLED_ShowString(2,1," ");
OLED_ShowString(2,1,Rx_Packet);
//根据接收的内容执行相应的动作
if(strcmp(Rx_Packet, "LED_ON")==0){
LED1_ON();
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,"LED_ON_OK");
SerialPort_SendString("LED_ON_OK\r\n");
}else if(strcmp(Rx_Packet, "LED_OFF")==0){
LED1_OFF();
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,"LED_OFF_OK");
SerialPort_SendString("LED_OFF_OK\r\n");
}else{
OLED_ShowString(4,1," ");
OLED_ShowString(4,1,"ERROR_COMMAND");
SerialPort_SendString("ERROR_COMMAND\r\n");
}
}
};
}
SerialPort.c新增函数(将上一节HEX数据包部分全部删除)
char SerialPort_RxPacket[100];
uint8_t SerialPort_RxPacketFlag = 0;
//获取接收的状态
uint8_t SerialPort_GetRxFlag(void){
if(SerialPort_RxPacketFlag==1){
SerialPort_RxPacketFlag = 0;
return 1;
}else{
return 0;
}
}
//获取接收的HEX数据包
char* SerialPort_GetRxPacket(void){
return SerialPort_RxPacket;
}
//USART1_RXNE中断函数
void USART1_IRQHandler(void){
uint8_t rec_byte;
static uint8_t rx_state;
static uint8_t rx_index;
if(USART_GetITStatus(USART1, USART_IT_RXNE)==SET){
rec_byte = USART_ReceiveData(USART1);
//利用状态机,接收HEX数据包
if(rx_state==0){
if(rec_byte== '@'){
rx_index = 0;
rx_state = 1;
}
}else if(rx_state==1){
if(rec_byte != '\r'){
SerialPort_RxPacket[rx_index] = rec_byte;
rx_index++;
}else{
rx_state = 2;
}
}else if(rx_state==2){
if(rec_byte == '\n'){
SerialPort_RxPacket[rx_index] = '\0';//字符串结束标志符
SerialPort_RxPacketFlag = 1;
rx_state = 0;
}
}
//读操作可以自动清零标志位,但加上也没事
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
//除了中断函数,其他函数还要在头文件SerialPort.h中声明
LED.c新增函数
/**
* @brief LED1亮
*/
void LED1_ON(void){
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}
/**
* @brief LED1灭
*/
void LED1_OFF(void){
GPIO_SetBits(GPIOA, GPIO_Pin_1);
}
//注意还要在头文件LED.h中声明