32单片机系列

51的学习暂时告一段落,也要继续学习stm32了。目前用的板子是江协的stm32f103c8的版本,先尝试着学习一下标准库的用法,虽然很难,但可以很好的理解计算机底层内容,尽力学吧,实在受不了就转向stmcubmax加hal库的方案。

1.GPIO

GPIO全拼叫General Purpose Input Output(通用输入输出)简称IO口,作用是用来控制连接在此GPIO口上的外设,通俗来说,就是单片机芯片通过控制IO口的电流输出,来起到控制外设的作用。

GPIO原理图

屏幕截图2024-12-12174243

一共有8种方式,但是一般常用输出方式就是推挽输出和开漏输出,而常用的输入方式是上拉输入和下拉输入,两者的区别就在于当没有外设给io口电平时,二者的默认电平不一样。

推挽输出同时支持高低电平驱动,方便快速切换电平,但是不支持线与(这个目前还没接触过),开漏输出就只支持低电平输入,电压取决于外部电压。

点灯+蜂鸣器

点灯

又是熟悉的点灯环节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stm32f10x.h"                  // Device header
int main()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //初始化时钟
GPIO_InitTypeDef GPIO_InitStructure; //定义gpio的一个结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //分别设置结构体的三个参数
GPIO_Init(GPIOA,&GPIO_InitStructure); //初始化GPIO
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET); //可以写入对应IO口的值
while(1)
{
}
}

效果图

A441DB3829A9723EC109E8F8ACA8D7E1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stm32f10x.h"   // Device header
#include "Delay.h"
int main()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
while(1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
Delay_ms(500);
}
}

这个就引入了延时函数,可以实现小灯的频闪,但是这个32的delay函数跟51一样,还是通过执行无用的指令来拖延时间。

蜂鸣器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stm32f10x.h"   // Device header
#include "Delay.h"
int main()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
while(1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
Delay_ms(500);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
Delay_ms(500);
}
}

本质上是一样的,无非就是改一下初始化的GPIO口,和调整对应的赋值。

然后蜂鸣器有三个接口,用公母线连接到面包板上,分别是vcc,gnd和控制音频的引脚。也是跟小灯一样的赋值即可。

GPIO输入

按钮控制点灯

首先是按钮驱动的配置

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
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
void Key_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
}
uint8_t Key_GetNum(void)
{
uint8_t KeyNum = 0;
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0)
{
Delay_ms(10);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1)==0);
KeyNum=1;
}
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0)
{
Delay_ms(10);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11)==0);
KeyNum=2;
}

return KeyNum;
}

都是先是正常的初始化函数,即设置GPIO参数,然后再写读取按钮函数,通过返回8位的int数据,来表示按钮的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "LED.h"
#include "key.h"
uint8_t KeyNum;
int main()
{
LED_Init();
Key_Init();
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum==1)
{
LED1_Turn();
}
if(KeyNum==2)
{
LED2_Turn();
}
}
}

这是主函数,逻辑很简单,就是通过判断Key_GetNum返回的值来判断按键的状态,其中,LED_Turn函数的内容就是如果GPIO输出为1就置为0,为0就置为1,的一个简单的反转函数。

光敏传感器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "stm32f10x.h"                  // Device header
void LightSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_13);
}
uint8_t LightSensor_Get(void)
{
return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}

都大同小异,光敏传感器驱动也是一样的初始化函数加一个读取函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "LED.h"
#include "key.h"
#include "Buzzer.h"
#include "LightSensor.h"
int main()
{
Buzzer_Init();
LightSensor_Init();
while(1)
{
if(LightSensor_Get()==1)
Buzzer_ON();
else
Buzzer_OFF();
}
}

这样就实现了当光不照射时,蜂鸣器一直响,当光照射时,蜂鸣器不响。

OLED

这个跟51单片机类似,用的是江科协的库,直接调用对应的函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
int main()
{
OLED_Init();
OLED_ShowChar(1,1,'A');
OLED_ShowString(1,3,"HelloWorld");
OLED_ShowNum(2,1,12345,5);
OLED_ShowSignedNum(2,7,-66,3);
OLED_ShowHexNum(3,1,0xAA55,4); //显示十六进制
OLED_ShowBinNum(4,1,0xAA55,16); //显示二进制
while(1)
{
}
}

底层

OLED所用的芯片SSD1306支持spi和iic通信,但是我的OLED只有两个iic通信接口,于是就是用iic通信了。

iic底层用的是软件模拟iic,不需要改动什么。

这款OLED是128*64的分辨率,所有的显示信息都储存在GDDRAM中,芯片会不断扫描这个GDDROM,将结果显示在OLED屏幕上,所以我们只需要对GDDRAM进行操作就可以了。

而这128*64又可以被分为 128*8byte,所以整个结构可以分为竖着的是128列,横着的是8页,一页就是一个字节,数据的存储就是一个字节的数据对用一列的一页,扫描也是从上到下,一页一页地扫描。

关键是拼凑SSD1306所需要的通信结构,

PixPin_2025-05-02_01-30-11

如上图所示,首先是起始信号,然后是从机地址跟读写位的设置,因为这个模块只写不读,所以这个字节直接设置为0x78就可以了,然后下一个数据是设置dc和连续性,其中dc置0就是命令模式,置1就是数据模式,co可以设置连续或者非连续发送,不过一般都是使用非连续发送。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void OLED_WriteCommand(uint8_t command)//写入命令的函数
{
Myi2c_start();
MyI2c_sendbyte(0x78);
MyI2c_receiveack();
MyI2c_sendbyte(0x00);
MyI2c_receiveack();
MyI2c_sendbyte(command);
MyI2c_receiveack();
Myi2c_stop();
}
void OLED_WriteData(uint8_t data)//写入数据的函数
{
Myi2c_start();
MyI2c_sendbyte(0x78);
MyI2c_receiveack();
MyI2c_sendbyte(0x40);
MyI2c_receiveack();
MyI2c_sendbyte(data);
MyI2c_receiveack();
Myi2c_stop();
}

上面是一些基础函数。

然后就可以基于基础函数构造一些高级的函数了。

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
104
105
106
107
108
109
110
111
112
void myoled_Init()//初始化函数,具体操作细节可以查看手册
{
MyI2C_Init();
Delay_ms(100);
OLED_WriteCommand(0xAE);
OLED_WriteCommand(0xD5);
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8);
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3);
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40);
OLED_WriteCommand(0xA1);
OLED_WriteCommand(0xC8);
OLED_WriteCommand(0xDA);
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81);
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9);
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB);
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4);
OLED_WriteCommand(0xA6);
OLED_WriteCommand(0x8D);
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF);
Delay_ms(100);
}
void OLED_SetCursor(uint8_t x,uint8_t page)//设置写入的位置,相当于设置鼠标光标位置
{
OLED_WriteCommand(0x00 | (x & 0x0F));
OLED_WriteCommand(0x10 | ((x & 0xF0) >> 4));
OLED_WriteCommand(0xB0 | page);
}
void OLED_Clear()//清屏函数
{
for(uint8_t j = 0;j<8;j++)
{
OLED_SetCursor(0,j);
for(uint8_t i = 0;i<128;i++)
{
OLED_WriteData(0x00);
}
}
}
void OLED_ShowChar(uint8_t x,uint8_t page,char ch,uint8_t fontsize)//展示单个字符
{

if(fontsize==6){
OLED_SetCursor(x,page);
for(uint8_t i = 0;i<6;i++)
{
OLED_WriteData(ascii_6x8[ch - ' '][i]);//ascii_6x8是一个二维数组,存放取模出来的ascall码
}
}
else if(fontsize==8)
{
OLED_SetCursor(x,page);
for(uint8_t i = 0;i<8;i++)
{
OLED_WriteData(OLED_F8_16[ch - ' '][i]);
}
OLED_SetCursor(x,page+1);
for(uint8_t i = 0;i<8;i++)
{
OLED_WriteData(OLED_F8_16[ch - ' '][i+8]);
}

}

}
void OLED_ShowString(uint8_t x,uint8_t page,char *ch,uint8_t fontsize)//遍历展示字符串
{
for(uint8_t i=0;ch[i] != '\0';i++)
{
OLED_ShowChar(x+i*fontsize,page,ch[i],fontsize);
}
}
void OLED_ShowImg(uint8_t x,uint8_t page,uint8_t width,uint8_t height,uint8_t *img)//显示图片
{
for(uint8_t j=0;j<height;j++)//这里的height准确来说应该是page
{
OLED_SetCursor(x,page+j);
for(uint8_t i=0;i<width;i++)
{
OLED_WriteData(img[width*j+i]);
}
}
}
void OLED_ShowChinese(uint8_t x,uint8_t page,char *Chinese)
{
char signalchinese[4] = {0};//存储单个中文
//char signalchinese[4] = "中"; // 实际存储为 {'\xE4', '\xB8', '\xAD', '\0'}
uint8_t pchinese = 0;
uint8_t pindex ;
for(uint8_t i = 0;Chinese[i]!='\0';i++)
{
signalchinese[pchinese] = Chinese[i];
pchinese++;
if(pchinese>=3){
pchinese=0;//到了3就可以拿出一个单个的中文出来了。
for(pindex=0;strcmp(OLED_CF_16_16[pindex].Index,"")!=0;pindex++){
if(strcmp(OLED_CF_16_16[pindex].Index,signalchinese) == 0)
{
break;//如果遍历字模库中发现有相同的中文,就立马退出循环,显示中文
}
}
OLED_ShowImg(x+((i+1)/3-1)*16),page,16,2,OLED_CF_16_16[pindex].Data);
}
}
}

这是关于中文字模的结构体定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef struct{
char Index[4];
uint8_t Data[32];
} ChineseCell_t;
const ChineseCell_t OLED_CF_16_16[]=
{
"你",
0x00,0x80,0x60,0xF8,0x07,0x40,0x20,0x18,0x0F,0x08,0xC8,0x08,0x08,0x28,0x18,0x00,
0x01,0x00,0x00,0xFF,0x00,0x10,0x0C,0x03,0x40,0x80,0x7F,0x00,0x01,0x06,0x18,0x00,/*,0*/
"好",
0x10,0x10,0xF0,0x1F,0x10,0xF0,0x00,0x80,0x82,0x82,0xE2,0x92,0x8A,0x86,0x80,0x00,
0x40,0x22,0x15,0x08,0x16,0x61,0x00,0x00,0x40,0x80,0x7F,0x00,0x00,0x00,0x00,0x00,/*,1*/
"世",
0x20,0x20,0x20,0xFE,0x20,0x20,0xFF,0x20,0x20,0x20,0xFF,0x20,0x20,0x20,0x20,0x00,
0x00,0x00,0x00,0x7F,0x40,0x40,0x47,0x44,0x44,0x44,0x47,0x40,0x40,0x40,0x00,0x00,/*,2*/
"界",
0x00,0x00,0x00,0xFE,0x92,0x92,0x92,0xFE,0x92,0x92,0x92,0xFE,0x00,0x00,0x00,0x00,
0x08,0x08,0x04,0x84,0x62,0x1E,0x01,0x00,0x01,0xFE,0x02,0x04,0x04,0x08,0x08,0x00,/*,3*/
" ",
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
};

将中文字模储存到OLED_CF_16_16里面就可以了。

以上的内容都是无缓冲区实现OLED显示的,无缓冲区的缺点就是不能在两个页码之间显示数据,只能上下两个页码同时写满。

中断

对射红外传感器计次

这里跟51单片机的定时器情况类似,32就直接引入了中断的概念,像我的stm32f103有68种中断函数,有硬件中断也有定时器中断还有EXIT中断等等。

1735054774083

这就是EXIT中断的简要流程图,我们也需要根据图上的内容来依次配置寄存器,用hal库。

首先第一个是AFIO,AFIO的作用是从多个GPIO接口中筛选出一个16接口来交给EXIT,避免线太多的麻烦,所以在配置EXIT中断时,选择监视的接口的端口位数不能有重复的,就比如不能同时设置GPIO_PinB1和GPIO_PinA1同时作为EXIT检测的io口,这样会出错。

然后就是EXIT的设置,跟GPIO的初始化配置类似,先是定义一个结构体,然后再交给hal库取值处理。

最后就是NVIC,这个东西的作用就是起到全局调度,设置好中断优先级以后,NVIC会配置好中断步骤,然后再交给。中断优先级可以分为抢占优先级和子优先级,其中抢占优先级是可以进行中断嵌套的,而子优先级的中断只能当本次中断执行结束后再执行。

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
#include "stm32f10x.h"                  // Device header
uint16_t CountSensor_Count=0;
void CountSensor_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14); //AFIO配置

EXTI_InitTypeDef EXTI_InitStructre;
EXTI_InitStructre.EXTI_LineCmd = ENABLE; //打开使能
EXTI_InitStructre.EXTI_Mode = EXTI_Mode_Interrupt; //设置中断模式还是事件模式
EXTI_InitStructre.EXTI_Trigger = EXTI_Trigger_Falling; //下降沿触发模式
EXTI_InitStructre.EXTI_Line = EXTI_Line14; //设置具体的线
EXTI_Init(&EXTI_InitStructre);

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;//NVIC通道
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//NVIC使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//NVIC权限设置,数字越小越先执行
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
}
uint16_t CountSensor_Get(void)
{
return CountSensor_Count;
}
void EXTI15_10_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line14)==SET)
{
CountSensor_Count++;
EXTI_ClearITPendingBit(EXTI_Line14);
}
}

这就是简单的一个光线传感器的计数模块,利用了中断函数进行计数。

旋转编码计次

这里用到了一个简单的旋转编码器,在连接vcc跟gnd和两个信号传输线后就可以正常使用了。有A相和B相输出,其中两向之间存在相位差,正转:如果A相先于B相变化,则表示编码器正转。反转:如果B相先于A相变化,则表示编码器反转。

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
#include "stm32f10x.h"                  // Device header

int16_t Encoder_Count;

void Encoder_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);

GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource0);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource1);

EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_Init(&EXTI_InitStructure);

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);

NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_Init(&NVIC_InitStructure);
}

int16_t Encoder_Get(void)
{
int16_t Temp;
Temp = Encoder_Count;
Encoder_Count = 0;
return Temp;
}

void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) == SET)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
Encoder_Count --;
}
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}

void EXTI1_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line1) == SET)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1) == 0)
{
if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0)
{
Encoder_Count ++;
}
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}

编码器核心代码如上,当触发中断的时候,快速判断另一个头是不是低电平,来实现判断正转和反转,并进行+1和-1的操作。在主程序中只用num+=Encoder_Get()即可,并打印出num的值。

定时器

定时器是stm32中的重要组成部分之一,跟51单片机有所不同,stm32中的定时器是从0开始向上计数,最大可以计数计到65535,我们要设置的通常是设置计到多少就触发中断,比如说可以设置1000-1,-1则是因为定时器的计数是从0开始计算的,所以需要-1才可以真实的达到1000.

先来张定时器原理图。

1743941133773

一般我们使用的都是内部时钟,这里的重点是时基单元的配置,第一是预分频器,预分频器就相当于把晶振的频率先做一次处理,然后再进行计数,如果计数的值达到了自动重装器的目标值,就进入中断。同样,定时器也与nvic的中断优先级的设置。

PWM

先上张PWM的原理图。

1743941133763

本质上就是在定时器的后面加上了一个输出比较。

1743941133769

这是输出比较模式的配置,基本上就是使用PWM模式1。

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
#include "stm32f10x.h"                  // Device header
void PWM_Init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure); //GPIo初始化

TIM_InternalClockConfig(TIM2);//设置TIM2为内部时钟源

TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;//设置时钟源分频
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;//设置为向上开始计数的模式
TIM_TimeBaseInitStruct.TIM_Period = 100-1;//自动重装的值,就是计数达到这个值触发中断
TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1;//预分频器的值
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;//高级定时器中的重复计数器,这里不用,设置为0
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct); //时基单元配置

TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);//初始化结构体,设置其他的为默认参数
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;//设置输出比较模式
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;//设置有效电平为高电平
TIM_OCInitStruct.TIM_Pulse = 0;//设置比较器,就是定时器计数到多少的时候触发
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OC1Init(TIM2,&TIM_OCInitStruct); //PWM模式配置

TIM_Cmd(TIM2,ENABLE); //开启定时器

}

void TIM_Set2Compare1(uint16_t compare)
{
TIM_SetCompare1(TIM2,compare); //设置PWM的比较器
}

上面的是PWM.c的配置,封装了一些基本的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "PWM.h"

int main()
{
PWM_Init();
while(1)
{
for(int i=0;i<=100;i++){
TIM_Set2Compare1(i);
Delay_ms(10);
}
for(int i=0;i<=100;i++){
TIM_Set2Compare1(100-i);
Delay_ms(10);
}
}
}

这是主函数的配置,通过delay加上合理设置PWM的输出比较器,最终实现呼吸灯的效果。

呼吸灯还有另一个改进的版本,就是使用了两个定时器来完成呼吸灯的操作,一个是用来输出PWM信号,还有一个定时器是用来延时,具体的作用就是上述第一个版本的定时器的这个代码。

1
Delay_ms(10);

具体函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void TIM3_IRQHandler(void) {
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET) {
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// 在此处添加需要定时执行的代码
if (pwmEnabled) {
static uint8_t direction = 0;
static uint16_t pwmValue = 0;
// 呼吸灯效果逻辑(示例)
if (direction == 0) {
pwmValue += 1;
if (pwmValue >= 100) direction = 1; // 假设PWM周期为1000
} else {
pwmValue -= 1;
if (pwmValue == 0) direction = 0;
}
// 更新PWM占空比(假设使用TIM2_CH1)
TIM_SetCompare1(TIM2, pwmValue);
}
}
}

就是这样子设置一个10ms的定时器,没10ms就会跑一下这个程序,这个10ms是至关重要的,如果不设置这个10ms,那么就不会有呼吸灯的效果了,就只会保持一个中等的亮度,有了人为加的10ms,延长了时间,人眼看上去就不是一个恒定的亮度。

ADC数模转换

ADC就是一种可以把连续的电信号转化为数字形式的离散信号的方式,像32单片机对ADC有着很好的支持,有专门的功能支持ADC的实现。

先放张ADC的配置过程图。

31E50140E4B6864A74B390B588510B62

首先还是先选择输入的模式,可以选GPIO的信号也可以选择内部外设的温度传感器作为信号源,这个对应的就是ADC的channel的选择,然后就进入规则组和注入组的配置还有一些基本的参数配置,这些也都是在官方库中以结构体的形式传递参数。然后可以选择模拟看门狗的配置,可以达到一定的值以后触发看门狗,挺方便的,也可以继续接入中断,触发中断去执行其他任务。

下面的触发控制可以选择定时器触发或者中断触发,也可以选择软件触发,我们使用的就是软件触发,RCC则用的是内部72mzh的时钟。

下面是具体代码

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
#include "stm32f10x.h"                  // Device header

void AD_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);

GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_0;
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure); //GPIO初始化

ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);//选择采样时间和频道,这里的频道设置要慎重选择,不同频道对应不用的io口

ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;//是否连续采样,就是不断循环读取值
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//选择触发源,这里的noone就是软件触发
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;//ADC模式,这里的是单ADC模式
ADC_InitStruct.ADC_NbrOfChannel = 1;//转换通道设置,就是你有多少条数据需要ADC转换
ADC_InitStruct.ADC_ScanConvMode = DISABLE;//扫描模式设置,选择扫描一个通道还是多个通道
ADC_Init(ADC1,&ADC_InitStruct);

ADC_Cmd(ADC1,ENABLE); //ADC使能

ADC_ResetCalibration(ADC1);//重置校验器
while(ADC_GetResetCalibrationStatus(ADC1)==SET);//等待重置完成
ADC_StartCalibration(ADC1);//启用校验器
while(ADC_GetCalibrationStatus(ADC1)==SET); //等待校验完成
}
uint16_t AD_GetValue()
{
ADC_SoftwareStartConvCmd(ADC1,ENABLE); //软件触发
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1); //读取转换的值
}

大致如上了,这些代码就可以实现对PA0口的输入的电压进行数模转换,如果想要调成其他io口,就需要翻手册,不同的对应的io口都有不同的channel值设置。

如果想对多个io口进行数模转化,也不难,对它们进行扫描就可以了。

1
2
3
4
5
6
7
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
ADC_RegularChannelConfig(ADC1,ADC_Channel,1,ADC_SampleTime_55Cycles5);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
while(ADC_GetFlagStatus(ADC1,ADC_FLAG_EOC)==RESET);
return ADC_GetConversionValue(ADC1);
}

可以将AD数据的读取函数设置成这个样子就可以了,在主函数里面依次读取不同channel的数值,并且储存到变量里面就可以了。

DMA

DMA是一种计算机系统中的数据传输技术,允许某些硬件子系统(如硬盘控制器、网络接口卡、声卡等)在不依赖中央处理器(CPU)的情况下,直接与系统的主内存(RAM)进行数据交换。这种技术的主要目的是提高数据传输效率 ,减轻CPU的负担。

PixPin_2025-04-09_15-11-49

这是DMA的核心原理图,我们需要配置的基本都在这个上面了。

下面是一个DMA使用案列。

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
#include "stm32f10x.h"                  // Device header
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t Addra,uint32_t Addrb,uint16_t Size)
{

MyDMA_Size = Size;

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = Size;//自动重装
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//方向设置,从外设作为出发地址
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;//设置为软件触发
DMA_InitStruct.DMA_MemoryBaseAddr = Addrb;//存储器地址
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器的数据宽度
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;//地址是否自增
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;//循环模式,有单次和无限循环的区别
DMA_InitStruct.DMA_PeripheralBaseAddr = Addra;//外设地址
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设的数据宽度
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设地址是否自增
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;//DMA级别设置,用于多个DMA之间选择先后
DMA_Init(DMA1_Channel1,&DMA_InitStruct);

DMA_Cmd(DMA1_Channel1,ENABLE);
}
void MyDMA_Transfor()
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
DMA_Cmd(DMA1_Channel1,ENABLE); //改变传输计数器的值,需要先失能DMA然后再使能才能生效
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1); //一旦查看了DMA的状态值就要记得清除
}

下面是主函数配置

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
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Mydma.h"
uint8_t dataa[]={1,2,3,4};
uint8_t datab[]={0,0,0,0};
int main()
{
OLED_Init();
MyDMA_Init((uint32_t)dataa,(uint32_t)datab,4);
while(1)
{
dataa[0]++;
dataa[1]++;
dataa[2]++;
dataa[3]++;
OLED_ShowHexNum(1,1,dataa[0],2);
OLED_ShowHexNum(1,4,dataa[1],2);
OLED_ShowHexNum(1,7,dataa[2],2);
OLED_ShowHexNum(1,10,dataa[3],2);
OLED_ShowHexNum(2,1,datab[0],2);
OLED_ShowHexNum(2,4,datab[1],2);
OLED_ShowHexNum(2,7,datab[2],2);
OLED_ShowHexNum(2,10,datab[3],2);
Delay_ms(1000);
MyDMA_Transfor();
OLED_ShowHexNum(3,1,dataa[0],2);
OLED_ShowHexNum(3,4,dataa[1],2);
OLED_ShowHexNum(3,7,dataa[2],2);
OLED_ShowHexNum(3,10,dataa[3],2);
OLED_ShowHexNum(4,1,datab[0],2);
OLED_ShowHexNum(4,4,datab[1],2);
OLED_ShowHexNum(4,7,datab[2],2);
OLED_ShowHexNum(4,10,datab[3],2);
Delay_ms(1000);
}
}

这样就可以清楚的看到DMA的传输数据的过程。

DMA与ADC相结合

在实际使用过程中,因为ADC数模转换的速度非常快,而且如果用的是规则组,只有一个寄存器来读取数据,所以我们需要DMA来帮助我们快速转运数据到其他位置,防止数据被覆盖而丢失。

虽然ST并没有写单个ADC通道转换完成,我们只能拿到一系列的ADC通道转换完成的返回,但是ST非常贴心的给出了ADC和DMA的交互通道,可以做到每个通道一转换完成,就可以通知DMA进行数据转移。

下面是一个使用案例。

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
#include "stm32f10x.h"                  // Device header
uint16_t data[2];
void MyDMA_Init()
{

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6);

GPIO_InitTypeDef GPIO_Initstructure;
GPIO_Initstructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Initstructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_Initstructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_Initstructure);

ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_ContinuousConvMode = DISABLE;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
ADC_InitStruct.ADC_NbrOfChannel = 2;
ADC_InitStruct.ADC_ScanConvMode = ENABLE;
ADC_Init(ADC1,&ADC_InitStruct);

ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);

DMA_InitTypeDef DMA_InitStruct;
DMA_InitStruct.DMA_BufferSize = 2;//自动重装(因为ADC开启了两个通道,所以DMA也转运两次)
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;//方向
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;//因为是硬件触发,所以设置disable
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&data[0];
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1,&DMA_InitStruct);

DMA_Cmd(DMA1_Channel1,ENABLE);
ADC_Cmd(ADC1,ENABLE);
ADC_DMACmd(ADC1,ENABLE);

ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1)==SET);
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1)==SET);
}
void AD_Getvalue()
{
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,2);
DMA_Cmd(DMA1_Channel1,ENABLE);
ADC_SoftwareStartConvCmd(ADC1,ENABLE);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}

这个代码的实现就需要把**AD_Getvalue()**这个函数放到while1里面不断循环设置DMA和软件触发ADC。

得到的数据就全部存在了data的数组里面,把这个数组在.h文件里面声明一下,就可以在主函数直接读取并且使用了。

其实还可以做一些配置,比如修改一下代码。

1
ADC_SoftwareStartConvCmd(ADC1,ENABLE);

1.把这个函数放在MyDMA_Init()里面;

2.然后将ADM和DMA都改成自动的。

1
2
DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;

这样子只要在调用一次MyDMA_Init()函数,就可以先软件触发ADC,然后ADC和DMA无限循环监测并转移数据了。

通信

先介绍几个通信的方式和概念吧。

Screenshot_2025-04-11-00-10-44-728_tv.danmaku.bil

解释一些概念:

  1. 双工:这是根据通信双方的传输方式决定的,分为单工,全双工和半双工,其中,单工就是只有单向通信,半双工指的是可以双向通信,但不能同时进行通信,而全双工则是可以同时进行双向通信。
  2. 时钟:异步指的是双方的通信不需要额外的时钟线,就是不需要时钟来同步数据传输,而同步则是需要时钟。
  3. 电平:就是指双方通信区分高低电平的方法,单端则是根据他们相对于同一个电平来区分,这个标准电平通常是GND,而差分电平就是通过两个电平的差分值来判断高低电平。差分的方式比较稳定。

这些都是一些比较常见的单片机传输协议,各有各的优点和特色吧。

USART

USART就是我们通常所说的串口通信。把hex文件烧录到单片机中,就使用了串口通信,和ch340这个usb转串口通信的芯片。

Screenshot_2025-04-11-00-28-17-826_tv.danmaku.bil

这是串口的一些参数和时序图,我们需要设置合适的参数。

一般选用的模式都是9字符带校验位的模式或者8字符不带校验位的模式。

PixPin_2025-04-12_01-05-08

这是USART的配置流程图,其中的TX和RX都是有与之相对应的GPIO口的,这些都是手册里写死的。

最开始都是先开启内部时钟并设置好分频以供发送和接收器的使用,并且发送和接受都有一个寄存器做好了缓存,避免数据覆盖,只有当数据完全写入或者读取后,才会写入新的数据。

发送

这是发送数据的函数代码。

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
#include "stm32f10x.h"                  // Device header
void Serial_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);

USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600;//设置波特率
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//关闭硬件流配置
USART_InitStruct.USART_Mode = USART_Mode_Tx;//选择模式
USART_InitStruct.USART_Parity = USART_Parity_No;//设置校验位
USART_InitStruct.USART_StopBits = USART_StopBits_1;//停止位设置
USART_InitStruct.USART_WordLength = USART_WordLength_8b;//设置数据大小
USART_Init(USART1,&USART_InitStruct);

USART_Cmd(USART1,ENABLE);
}
void Serial_SendByte(uint8_t byte)
{
USART_SendData(USART1,byte);
while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
}
void Serial_SendArray(uint8_t *array,uint16_t length)
{
for(uint16_t i = 0;i<length;i++)
{
Serial_SendByte(array[i]);
}
}
void Serial_SendString(char *String)
{
for(uint8_t i = 0;String[i]!='\0';i++)
{
Serial_SendByte(String[i]);
}
}

接收

接收和发送大致相同,就改了几行的配置。

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
#include "stm32f10x.h"                  // Device header
void Serial_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);

USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;//加上RX模式
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStruct);

USART_Cmd(USART1,ENABLE);
}

主函数里的while1里面再这样写

1
2
3
4
5
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
{
data = USART_ReceiveData(USART1);
}
OLED_ShowHexNum(1,1,data,2);

就可以实现对接收到的数据进行读取的操作了。

还可以使用中断,只需要加上这行

1
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

并且配置好nvic就可以使用中断了。

USART发送接收数据包

前文的发送和接收的都是只能发送和接收单字节,下面将实现对一个数据包的发送和接收。因为发送比较简单,拼出发送一个字符串就可以了。所有主要写的还是接收数据包。

首先先说明一下数据包。

数据包有两种形式,一个是固定长度,还有一个是不固定长度,但是不论哪种形式,都需要定义包头和包尾,发送的内容也可以是hex模式(即16进制的模式),和文本模式,即发送已经编码后的文本。

下面是一张不固定长度的文本模式的数据包构造,可以看到图中的包头是“ @ ” , 包尾是 “\r\n”(这是windows系统的换行符) ,图中也写明了如何定义接收的函数,就是通过状态机的思想,通过不同的状态来控制整个的接收流程。

Screenshot_2025-04-14-11-20-55-840_tv.danmaku.bil

下面是一个程序实例。

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
#include "stm32f10x.h"                  // Device header
uint8_t USARTStatu;
uint8_t RXPack[4];
uint8_t TXPack[4];
void Serial_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);

USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStruct);

USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);

USART_Cmd(USART1,ENABLE);
}
uint8_t USART_Getflag()
{
if(USARTStatu==1)
{
USARTStatu=0;
return 1;
}
else
return 0;
}
void USART1_IRQHandler()
{
static uint8_t RxState=0;
static uint8_t pRxState=0;
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
{
uint8_t Rxdata = USART_ReceiveData(USART1);
if(RxState==0)
{
if(Rxdata==0xFF)
RxState=1;
}
else if(RxState==1)
{
RXPack[pRxState] = Rxdata;
pRxState++;
if(pRxState>=4)
{
RxState=2;
}
}
else if(RxState==2)
{
if(Rxdata==0xFE)
{
RxState = 0;
USARTStatu=1;
}
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);//清楚标志位
}
}

这是一个固定长度的hex数据包的接收,其中包头是 “0xFF” ,包尾是 “0xFE” 。

1
2
3
4
5
6
7
if(USART_Getflag()==1)
{
OLED_ShowHexNum(1,1,RXPack[0],2);
OLED_ShowHexNum(1,3,RXPack[1],2);
OLED_ShowHexNum(1,5,RXPack[2],2);
OLED_ShowHexNum(1,7,RXPack[3],2);
}

在主函数的while循环里执行这条语句,就可以不断读取接收到的数据,并且显示在oled屏幕上了。

还有无固定大小文本模式。

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
#include "stm32f10x.h"                  // Device header
uint8_t USARTStatu=0;
char data[100];
uint8_t RXPack[4];
uint8_t TXPack[4];
void Serial_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOA, &GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_Init(GPIOA, &GPIO_InitStructure);

USART_InitTypeDef USART_InitStruct;
USART_InitStruct.USART_BaudRate = 9600;
USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStruct.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_InitStruct.USART_Parity = USART_Parity_No;
USART_InitStruct.USART_StopBits = USART_StopBits_1;
USART_InitStruct.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1,&USART_InitStruct);

USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStruct);

USART_Cmd(USART1,ENABLE);
}
void USART1_IRQHandler()
{
static uint8_t RxState=0;
static uint8_t pRxState=0;
if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==SET)
{
uint8_t Rxdata = USART_ReceiveData(USART1);
if(RxState==0)
{
if(Rxdata=='@' && USARTStatu==0)
RxState=1;
}
else if(RxState==1)
{
if(Rxdata=='\r')
RxState=2;
else{
data[pRxState] = Rxdata;
pRxState++;
}
}
else if(Rxdata=='\n')
{
RxState = 0;
data[pRxState] = '\0';
pRxState=0;
USARTStatu=1;
}
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
}
}

这个有效地防止了数据包的错位,直接引入了USARTStatu的判定。

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
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
#include "Serial.h"
#include "string.h"
int main()
{
OLED_Init();
Serial_Init();
LED_Init();
while(1)
{
if(USARTStatu==1)
{
OLED_ShowString(1,1,data);
if(strcmp(data,"LEDON")==0)
{
LED1_ON();
}
else if(strcmp(data,"LEDOF")==0)
{
LED1_OFF();
}
USARTStatu=0;
}
}
}

在主函数中做一下数据的处理,就可以很方便的完成电脑输入命令然后对里面的内容进行控制了。

IIC通信

IIC是一种通信协议,是一个同步半双工通信,用途非常广,只需要SCL和SDA两根线就可以完成数据的同步传输,因为是同步的协议,所以可以随时开始通信也可以随时停止通信,而且可以一个主多从,一个主机可以和多个从机进行交互。

先介绍一下基本的通信流程吧。

一开始初始因为有一个上拉电阻,处于一个弱上拉的状态,所以呈现的状态是高电平。

PixPin_2025-04-23_11-43-18

这是开始和结束的时序图,当主机准备开始通信的时候,就会在保持SCL高电平的同时,拉低SDA。主机在准备结束通信的时候,就会在SCL保持高电平的时候,回弹SDA。

PixPin_2025-04-23_12-07-23

这是发送一个字节的时序。 在SCL在低电平的时间,主机快速把数据放在SDA上,等到SCL回弹到高电平之后,从机就会读取SDA的电平状态来判断0还是1,然后主机再拉低SCL,如此反复执行8次,就可以发送一个字节的时序。

PixPin_2025-04-23_12-25-47

这是接收一个字节的时序。在SCL在低电平的时间,从机快速把数据放在SDA上,主机再把SCL置为高电平并且此时读取SDA的电平状态判断是0还是1,也是循环8次,接收一个字节(主机在接收前需要回弹SDA)。

PixPin_2025-04-23_13-17-07

这是发送应答和接收应答的时序。当每完成一个字节的数据传输以后,就会开启应答来确认一下是否接收到了数据。其中,在主机接收应答前,需要把SDA的电平拉下来,然后从机如果接收到了数据,需要立马将SDA回弹至高电平,只有这样才算接收到了数据。

这些就是IIC通信的基本单元,下面是一个完整的IIC发送流程:

  1. 主机先发送开始信号。
  2. 主机继续发送选择从机的地址码加上发送还是接收选择,其中,地址码通常是一个7位的二进制数据,然后模式选择是选择发送还是接收,其中0表示写,1表示读,拼接起来就是一个8位一个字节的数据。
  3. 然后就是发送数据,发送一个字节的数据,从机依次接收数据。(这个数据可以是需要写入的地址或者写入的数据,这个具体看从机的要求)
  4. 从机发送ack表示已经接收到了数据。
  5. 最后主机可以选择发送stop终止信号。

下面是一个完整的IIC接收流程:

  1. 主机先发送开始信号。
  2. 一样的主机发送从机的地址码加一个0。
  3. 发送你要读取的寄存器的地址,从机会应答。
  4. 重复起始条件。
  5. 发送从机地址加1,切换到读的模式。
  6. 从机控制SDA线,主机读取SDA线以获取数据,如果需要继续发送,主机发送ACK就行了,如果终止发送,主机发送NACK。
  7. 最后主机发送stop终止信号。

软件模拟IIC

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
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

void Myi2c_W_SCL(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_10,(BitAction)Bitvalue);
Delay_us(10);
}
void Myi2c_W_SDA(uint8_t Bitvalue)
{
GPIO_WriteBit(GPIOB,GPIO_Pin_11,(BitAction)Bitvalue);
Delay_us(10);
}
uint8_t Myi2c_R_SDA()//封装了一些基本的函数
{
uint8_t Bitvalue;
Bitvalue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
Delay_us(10);
return Bitvalue;
}
void MyI2C_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_10 | GPIO_Pin_11);
}
void Myi2c_start()
{
Myi2c_W_SDA(1);
Myi2c_W_SCL(1);
Myi2c_W_SDA(0);
Myi2c_W_SCL(0);
}
void Myi2c_stop()
{
Myi2c_W_SDA(0);
Myi2c_W_SCL(1);
Myi2c_W_SDA(1);
}
void MyI2c_sendbyte(uint8_t Byte)
{
uint8_t i;
for(i=0;i<8;i++)
{
Myi2c_W_SDA(Byte & (0x80>>i));
Myi2c_W_SCL(1);
Myi2c_W_SCL(0);
}
}
uint8_t MyI2c_receivebyte()//发送一个字节的函数
{
uint8_t i,Byte = 0x00;
Myi2c_W_SDA(1);
for(i=0;i<8;i++)
{
Myi2c_W_SCL(1);
if(Myi2c_R_SDA() == 1){Byte |= (0x80>>i);};
Myi2c_W_SCL(0);
}
return Byte;
}
void MyI2c_sendack(uint8_t ackbit)
{
Myi2c_W_SDA(ackbit);
Myi2c_W_SCL(1);
Myi2c_W_SCL(0);
}
uint8_t MyI2c_receiveack()
{
uint8_t ackbit;
Myi2c_W_SDA(1);
Myi2c_W_SCL(1);
ackbit = Myi2c_R_SDA();
Myi2c_W_SCL(0);
return ackbit;
}

这是软件模拟IIC的基本函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "myiic.h"
#include "OLED.h"

int main()
{
OLED_Init();
MyI2C_Init();
Myi2c_start();
MyI2c_sendbyte(0xD0);
uint8_t ack = MyI2c_receiveack();
Myi2c_stop();
OLED_ShowNum(1,1,ack,3);
while(1)
{
}
}

这是一个简单的使用例子,发送了选择从机的字节后接收回应,正确的显示应该是000。

IIC加MPU6050

MPU6050是一个比较出名的运动处理传感器,它有着三轴陀螺仪传感器和三轴加速度传感器,可以测算出芯片的姿态,应用非常广泛,其中它的通信方式就是通过IIC与主机进行通信。

下面是基于上节所写的IIC基本代码而完成的MPU6050库函数编写。

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
#include "stm32f10x.h"                  // Device header
#include "myiic.h"
#include "MPU6050_Reg.h"
#define MPU6050_ADDRESS 0xD0
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
Myi2c_start();
MyI2c_sendbyte(MPU6050_ADDRESS);
MyI2c_receiveack();
MyI2c_sendbyte(RegAddress);
MyI2c_receiveack();
MyI2c_sendbyte(Data);
MyI2c_receiveack();
Myi2c_stop();
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;

Myi2c_start();
MyI2c_sendbyte(MPU6050_ADDRESS);
MyI2c_receiveack();
MyI2c_sendbyte(RegAddress);
MyI2c_receiveack();//第一次发送,将地址指针移动到想要读取数据的位置

Myi2c_start();
MyI2c_sendbyte(MPU6050_ADDRESS | 0x01);
MyI2c_receiveack();
Data = MyI2c_receivebyte();
MyI2c_sendack(1);
Myi2c_stop();

return Data;
}

void MPU6050_Init()
{
MyI2C_Init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);//根据手册写的基本的MPU6050的寄存器配置
}
void MPU6050_Getdata(int16_t *AccX,int16_t *AccY,int16_t *AccZ,int16_t *GyroX,int16_t *GyroY,int16_t *GyroZ)
{
uint8_t DataH,DataL;

DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH<<8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH<<8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH<<8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH<<8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH<<8) | DataL;

DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH<<8) | DataL;

}//读取对应的寄存器的值

调用的话就在主函数里面声明这些变量,然后while里面不断获取数据就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "myiic.h"
#include "OLED.h"
#include "MPU6050.h"

int main()
{
OLED_Init();
MPU6050_Init();
int16_t ax,ay,az,gx,gy,gz;
while(1)
{
MPU6050_Getdata(&ax,&ay,&az,&gx,&gy,&gz);
OLED_ShowSignedNum(2,1,ax,5);
OLED_ShowSignedNum(3,1,ay,5);
OLED_ShowSignedNum(4,1,az,5);
OLED_ShowSignedNum(2,8,gx,5);
OLED_ShowSignedNum(3,8,gy,5);
OLED_ShowSignedNum(4,8,gz,5);
}
}

这样子就可以在OLED上看到不断打印出的数据了。

硬件模拟IIC

stm32f103系列自带了有两个IIC的外设,对应的是IIC1和IIC2,硬件控制比软件模拟就相对舒服很多了,库函数也提供了很成熟的函数支持IIC通信,与GPIO等常用外设初始化一样,IIC的初始化也是通过构造结构体并且传入数值来进行设置。

Screenshot_2025-04-24-09-00-29-587_tv.danmaku.bil

这是IIC的配置流程,基本上按照流程图配置完寄存器就可以了。

IIC外设的寄存器分为三种,第一个是CR,控制寄存器,就是对IIC外设进行基本设置的寄存器,第二个是DR,数据寄存器,就是存储IIC接收到的数据的寄存器,不管是发送还是接收数据,都会用到这个DR寄存器,第三个是SR,状态寄存器,专门用来读取IIC的状态的,方便对IIC的工作做出基本的判断和调控。

PixPin_2025-04-25_13-22-47

这是发送时序的流程图,其中EVx就是可以通过状态寄存器SR进行读取的。

PixPin_2025-04-25_13-23-07

这是接收器的时序图,写代码按照这个图上的流程进行基本的配置就可以了,st官方的库函数已经封装好了相对成熟的函数方便我们进行操作。

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
#include "stm32f10x.h"                  // Device header
#include "myiic.h"
#include "MPU6050_Reg.h"
#define MPU6050_ADDRESS 0xD0
void MPU6050_WriteReg(uint8_t RegAddress,uint8_t Data)
{
I2C_GenerateSTART(I2C2,ENABLE);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
I2C_SendData(I2C2,RegAddress);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
I2C_SendData(I2C2,Data);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTING) != SUCCESS);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
I2C_GenerateSTOP(I2C2,ENABLE);
}
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2,ENABLE);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Transmitter);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED) != SUCCESS);
I2C_SendData(I2C2,RegAddress);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_TRANSMITTED) != SUCCESS);
I2C_GenerateSTART(I2C2,ENABLE);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_MODE_SELECT) != SUCCESS);
I2C_Send7bitAddress(I2C2,MPU6050_ADDRESS,I2C_Direction_Receiver);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED) != SUCCESS);
I2C_AcknowledgeConfig(I2C2,DISABLE);
I2C_GenerateSTOP(I2C2,ENABLE);
while(I2C_CheckEvent(I2C2,I2C_EVENT_MASTER_BYTE_RECEIVED) != SUCCESS);
Data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2,ENABLE);
return Data;
}

void MPU6050_Init()
{
// I2C_Init();
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStructure);

I2C_InitTypeDef I2C_InitStruct;
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStruct.I2C_ClockSpeed = 10000;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_16_9;
I2C_InitStruct.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C2,&I2C_InitStruct);

I2C_Cmd(I2C2,ENABLE);

MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);
MPU6050_WriteReg(MPU6050_CONFIG,0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
}

这是针对上一节的软件模拟IIC实现MPU6050的读取的优化,如果担心while会陷入死循环的话,可以再封装一个等待函数,可以尝试10000–这种操作,如果超时就强行退出。

spi通信

spi通信有四根线,分别是MOSI,MOSO,SLK,SS。

名称 功能
MOSI 主机发送
MOSO 主机接收
SLK 时钟线,由主机完全控制
SS 设备选择线,低电平表示选中设备

spi相对iic来说要简单一些,因为它的线比较多,所以没有这么多限制条件,下图是spi通信的原理,本质上发送和接收是一体的,同时发送同时接收,就是通过一位一位的移,差不多是一个循环模式。

Screenshot_2025-04-27-09-17-16-594_tv.danmaku.bil

PixPin_2025-04-30_10-55-23

这是最常用的模式0的时序,模式0就是上升沿读取数据,下降沿准备数据,并且SCK空闲的时候为低电平,总共有四种模式选择,就是CPOL和CPHA的不同组合,最常用的还是模式0。

软件模拟spi

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
#include "stm32f10x.h"                  // Device header
void myspi_w_ss(uint8_t bitvalue){

GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)bitvalue);
}
void myspi_w_sck(uint8_t bitvalue){

GPIO_WriteBit(GPIOA,GPIO_Pin_5,(BitAction)bitvalue);
}
void myspi_w_mosi(uint8_t bitvalue){

GPIO_WriteBit(GPIOA,GPIO_Pin_7,(BitAction)bitvalue);
}
uint8_t myspi_r_miso()
{
return GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_6);
}
void myspi_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5| GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

myspi_w_ss(1);
myspi_w_sck(0);
}
void myspi_start()
{
myspi_w_ss(0);
}
void myspi_stop()
{
myspi_w_ss(1);
}
uint8_t myspi_swapbyte(uint8_t bytesend) {
uint8_t received_byte = 0;
for (uint8_t i = 0; i < 8; i++) {
// 发送最高位
myspi_w_mosi((bytesend & 0x80) ? 1 : 0);
// 拉高时钟,从设备在上升沿采样MOSI数据
myspi_w_sck(1);
// 读取MISO数据(在时钟高电平期间有效)
received_byte <<= 1;
if (myspi_r_miso() == 1) {
received_byte |= 0x01;
}
// 拉低时钟,准备下一位
myspi_w_sck(0);
// 准备发送下一位
bytesend <<= 1;
}
return received_byte;
}

通过myspi_swapbyte函数,就可以交换一个字节,以达到通信的效果。

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
#include "stm32f10x.h"                  // Device header
#include "myspi.h"
#include "w25q64_ins.h"
void w25q64_Init()
{
myspi_Init();
}
void w25q64_readid(uint8_t *MID,uint16_t *DID)
{
myspi_start();
myspi_swapbyte(W25Q64_JEDEC_ID);
*MID = myspi_swapbyte(W25Q64_DUMMY_BYTE);
*DID = myspi_swapbyte(W25Q64_DUMMY_BYTE);
*DID <<=8;
*DID |= myspi_swapbyte(W25Q64_DUMMY_BYTE);
myspi_stop();

}
void w25q64_write_enable()
{
myspi_start();
myspi_swapbyte(W25Q64_WRITE_ENABLE);
myspi_stop();
}
void w25q64_wait_busy()
{
myspi_start();
myspi_swapbyte(W25Q64_READ_STATUS_REGISTER_1);
while((myspi_swapbyte(W25Q64_DUMMY_BYTE)& 0x01)==0x01);
myspi_stop();

}
void w25q64_pageprogram(uint32_t address,uint8_t *data,uint16_t count)
{
w25q64_write_enable();
myspi_start();
myspi_swapbyte(W25Q64_PAGE_PROGRAM);
myspi_swapbyte(address>>16);
myspi_swapbyte(address>>8);
myspi_swapbyte(address);
uint16_t i;
for(i=0;i<count;i++)
{
myspi_swapbyte(data[i]);
}
myspi_stop();
w25q64_wait_busy();
}
void w25q64_sector_erase(uint32_t address)
{
w25q64_write_enable();
myspi_start();
myspi_swapbyte(W25Q64_SECTOR_ERASE_4KB);
myspi_swapbyte(address>>16);
myspi_swapbyte(address>>8);
myspi_swapbyte(address);
myspi_stop();
w25q64_wait_busy();
}
void w25q64_readdata(uint32_t address,uint8_t *data,uint32_t count)
{
myspi_start();
myspi_swapbyte(W25Q64_READ_DATA);
myspi_swapbyte(address>>16);
myspi_swapbyte(address>>8);
myspi_swapbyte(address);
uint32_t i;
for(i=0;i<count;i++)
{
data[i] = myspi_swapbyte(W25Q64_DUMMY_BYTE);
}
myspi_stop();

}

这是基于spi底层所写的关于w25q64的使用函数封装,spi通信很多都是按照对应的外设发送设定好的命令来做一些交互。

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
#include "stm32f10x.h"   // Device header
#include "Delay.h"
#include "LED.h"
#include "OLED.h"
#include "w25q64.h"
uint8_t MID;
uint16_t DID;
uint8_t dataw[] = {0x01,0x02,0x03,0x04};
uint8_t datar[4];
int main()
{
OLED_Init();
w25q64_Init();
w25q64_readid(&MID,&DID);
OLED_ShowHexNum(1,1,MID,2);
OLED_ShowHexNum(1,5,DID,4);
w25q64_sector_erase(0x000000);
w25q64_pageprogram(0x0000000,dataw,4);
w25q64_readdata(0x000000,datar,4);
OLED_ShowHexNum(2,1,datar[0],2);
OLED_ShowHexNum(2,4,datar[1],2);
OLED_ShowHexNum(2,7,datar[2],2);
OLED_ShowHexNum(2,10,datar[3],2);
while(1)
{
}
}

这是调用方法,可以看到数据成功被移入w25q64中,并且断电也没有丢失。

硬件模拟spi

PixPin_2025-05-02_00-21-49

stm32的库函数已经帮我们处理好了很多细节上的配置,基本上按照流程图加结构体配置,就可以使用硬件模拟spi了。

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
#include "stm32f10x.h"                  // Device header
void myspi_w_ss(uint8_t bitvalue){
GPIO_WriteBit(GPIOA,GPIO_Pin_4,(BitAction)bitvalue);
}
void myspi_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);

GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5| GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);

SPI_InitTypeDef SPI_InitStruct;
SPI_InitStruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;
SPI_InitStruct.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStruct.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStruct.SPI_CRCPolynomial = 7;
SPI_InitStruct.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStruct.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStruct.SPI_Mode = SPI_Mode_Master;
SPI_InitStruct.SPI_NSS = SPI_NSS_Soft;
SPI_Init(SPI1, &SPI_InitStruct);

SPI_Cmd(SPI1,ENABLE);

myspi_w_ss(1);
}
void myspi_start()
{
myspi_w_ss(0);
}
void myspi_stop()
{
myspi_w_ss(1);
}

uint8_t myspi_swapbyte(uint8_t bytesend) {
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);
SPI_I2S_SendData(SPI1, bytesend);
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);
return SPI_I2S_ReceiveData(SPI1);
}

这是硬件模拟spi的底层代码,与其他外设大同小异,都是结构体配置还有什么状态读取之类的。

CAN通信

can通信在四大通信里面算是最难的一款了通信协议了,它兼具IIC和USART通信的一些特点,一样有TX和RX两根线,没有时钟线,全双工,但是又可以一对多进行数据通信,多用于板子间的数据交流。

而且can通信有非常完善的数据纠错等等功能,可以很好地处理异常,错误等各种情况。

BB395BB43C68A69FAFFD812AB538B50A

这是CAN总线的几种帧格式,用的最多的还是数据帧和遥控帧。

64F3FE0F7788F7928F7336A3275267F7

这是can通信的时序,分为标准格式和扩展格式两种,标准格式和扩展格式的区别是扩展格式的ID可以更长一点,扩展格式id有29位。

不过平常还是使用标准格式比较多,毕竟没这么多设备。

64570E254949F2F00597014011FA048A

可以对着上图的简介表理解时序图,can通信每一次发送最多可以发送8字节。

还有就是can通信的仲裁段,如果碰到两个设备同时准备发送通信,就会进入仲裁阶段,仲裁的依据主要就是id号的大小,id号越小的仲裁获胜的可能性越大,并且标准大于扩展,数据帧大于遥控帧,这里的位数设计非常巧妙,很好地兼容了标准格式和扩展格式两种情况。

下面是一些can总线为我传输不出错更严谨的独特机制。

DA7195F27407D4D7F1D9D27E8227B05F

首先是位填充,为了避免发送大量相同的数据造成误判的可能情况,所以设计了位填充的机制,就是每发5个相同电平时,会自动在后面追加一个相反电平的填充位,具体机制可以看上图。

这个可以防止发送大量相同的电平,比如连续发送隐性1,被误判为进入了空闲状态,(连续发送11个隐性1就是进入了空闲状态,11位其实就是时序图中的ack界定符加上7的个结束位加上3个的帧间隔)

631928DB379DFC98E67C46FB3CA2C9BA

还有就是关于时序上的一些知识点,其中每一个位的采集又可以细分为4个部分,其中ss是同步段,就是每次一开始发数据,每个设备都会利用ss来进行同步,确保自己的初始时钟是正确的,这就是硬同步。

0872D0C959DE12E5DAC9E9ED236A4335

pts是传播时间段,这个通常有硬件问题而配置的,毕竟传输过来还需要时间。

然后就是pbs1和pbs2,这两个就是控制采样位的核心了,也是再同步。

2ED55889A566C8C77329F97A8C0A7B78

就是通过设置sjw,sjw会在通信的过程中,保证采样点在pbs1和pbs2之间,不过这些都是硬件电路自动完成的,这就是can通信的再同步机制。

硬件调用CAN通信

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
#include "stm32f10x.h"                  // Device header
void Mycan_Init()
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1,ENABLE);

GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);

GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);

CAN_InitTypeDef CAN_InitStruct;
CAN_InitStruct.CAN_ABOM = DISABLE;
CAN_InitStruct.CAN_AWUM = DISABLE;
CAN_InitStruct.CAN_BS1 = CAN_BS1_2tq;
CAN_InitStruct.CAN_BS2 = CAN_BS2_3tq;
CAN_InitStruct.CAN_Mode = CAN_Mode_LoopBack;
CAN_InitStruct.CAN_NART = DISABLE;
CAN_InitStruct.CAN_Prescaler = 48;
CAN_InitStruct.CAN_RFLM = DISABLE;
CAN_InitStruct.CAN_SJW = CAN_SJW_2tq;
CAN_InitStruct.CAN_TTCM = DISABLE;
CAN_InitStruct.CAN_TXFP = DISABLE;

CAN_Init(CAN1,&CAN_InitStruct);

CAN_FilterInitTypeDef CAN_FilterInitStruct;
CAN_FilterInitStruct.CAN_FilterActivation = ENABLE;
CAN_FilterInitStruct.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStruct.CAN_FilterIdHigh = 0x0000;
CAN_FilterInitStruct.CAN_FilterIdLow = 0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdHigh = 0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInitStruct.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStruct.CAN_FilterNumber = 0;
CAN_FilterInitStruct.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInit(&CAN_FilterInitStruct);
}
void Mycan_transmit(uint32_t id,uint8_t length,uint8_t *data)
{
CanTxMsg TxMessage;
for(uint8_t i=0;i<length;i++)
{
TxMessage.Data[i] = data[i];
}
TxMessage.DLC = length;
TxMessage.ExtId = id;
TxMessage.IDE = CAN_Id_Standard;
TxMessage.RTR = CAN_RTR_DATA;
TxMessage.StdId = id;
uint8_t transmitmailbox = CAN_Transmit(CAN1,&TxMessage);
while(CAN_TransmitStatus(CAN1,transmitmailbox)!=CAN_TxStatus_Ok);
}
uint8_t Mycan_receiveflag()
{
if(CAN_MessagePending(CAN1,CAN_FIFO0)>0)
return 1;
return 0;
}
void Mycan_receive(uint32_t *id,uint8_t *length,uint8_t *data)
{
CanRxMsg RxMessage;
CAN_Receive(CAN1,CAN_FIFO0,&RxMessage);
if(RxMessage.IDE==CAN_Id_Standard)
{
*id=RxMessage.StdId;
}
else
{
*id=RxMessage.ExtId;
}
if(RxMessage.RTR==CAN_RTR_DATA)
{
*length = RxMessage.DLC;
for(uint8_t i=0;i<*length;i++)
{
data[i]=RxMessage.Data[i];
}
}
else
{
}
// RxMessage.FMI = ;
}

我这里也只是测试了一下回环通信,测试一下软件有没有问题。