32单片机系列
51的学习暂时告一段落,也要继续学习stm32了。目前用的板子是江协的stm32f103c8的版本,先尝试着学习一下标准库的用法,虽然很难,但可以很好的理解计算机底层内容,尽力学吧,实在受不了就转向stmcubmax加hal库的方案。
1.GPIO
GPIO全拼叫General Purpose Input Output(通用输入输出)简称IO口,作用是用来控制连接在此GPIO口上的外设,通俗来说,就是单片机芯片通过控制IO口的电流输出,来起到控制外设的作用。
GPIO原理图

一共有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_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) { } }
|
效果图

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include "stm32f10x.h" #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" #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" #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" #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" 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" #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所需要的通信结构,

如上图所示,首先是起始信号,然后是从机地址跟读写位的设置,因为这个模块只写不读,所以这个字节直接设置为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]); } } 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++) { 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}; 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; 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, "好", 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, "世", 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, "界", 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, " ", 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中断等等。

这就是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" 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); 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_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 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"
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.
先来张定时器原理图。

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

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

这是输出比较模式的配置,基本上就是使用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" 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); TIM_InternalClockConfig(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; 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); TIM_Cmd(TIM2,ENABLE);
}
void TIM_Set2Compare1(uint16_t compare) { TIM_SetCompare1(TIM2,compare); }
|
上面的是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 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; } else { pwmValue -= 1; if (pwmValue == 0) direction = 0; } TIM_SetCompare1(TIM2, pwmValue); } } }
|
就是这样子设置一个10ms的定时器,没10ms就会跑一下这个程序,这个10ms是至关重要的,如果不设置这个10ms,那么就不会有呼吸灯的效果了,就只会保持一个中等的亮度,有了人为加的10ms,延长了时间,人眼看上去就不是一个恒定的亮度。
ADC数模转换
ADC就是一种可以把连续的电信号转化为数字形式的离散信号的方式,像32单片机对ADC有着很好的支持,有专门的功能支持ADC的实现。
先放张ADC的配置过程图。

首先还是先选择输入的模式,可以选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"
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); ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5); 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 = 1; ADC_InitStruct.ADC_ScanConvMode = DISABLE; ADC_Init(ADC1,&ADC_InitStruct); ADC_Cmd(ADC1,ENABLE); 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的负担。

这是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" 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_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); while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET); DMA_ClearFlag(DMA1_FLAG_TC1); }
|
下面是主函数配置
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" 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; DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStruct.DMA_M2M = DMA_M2M_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无限循环监测并转移数据了。
通信
先介绍几个通信的方式和概念吧。

解释一些概念:
- 双工:这是根据通信双方的传输方式决定的,分为单工,全双工和半双工,其中,单工就是只有单向通信,半双工指的是可以双向通信,但不能同时进行通信,而全双工则是可以同时进行双向通信。
- 时钟:异步指的是双方的通信不需要额外的时钟线,就是不需要时钟来同步数据传输,而同步则是需要时钟。
- 电平:就是指双方通信区分高低电平的方法,单端则是根据他们相对于同一个电平来区分,这个标准电平通常是GND,而差分电平就是通过两个电平的差分值来判断高低电平。差分的方式比较稳定。
这些都是一些比较常见的单片机传输协议,各有各的优点和特色吧。
USART
USART就是我们通常所说的串口通信。把hex文件烧录到单片机中,就使用了串口通信,和ch340这个usb转串口通信的芯片。

这是串口的一些参数和时序图,我们需要设置合适的参数。
一般选用的模式都是9字符带校验位的模式或者8字符不带校验位的模式。

这是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" 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" 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_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系统的换行符) ,图中也写明了如何定义接收的函数,就是通过状态机的思想,通过不同的状态来控制整个的接收流程。

下面是一个程序实例。
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" 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" 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" #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两根线就可以完成数据的同步传输,因为是同步的协议,所以可以随时开始通信也可以随时停止通信,而且可以一个主多从,一个主机可以和多个从机进行交互。
先介绍一下基本的通信流程吧。
一开始初始因为有一个上拉电阻,处于一个弱上拉的状态,所以呈现的状态是高电平。

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

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

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

这是发送应答和接收应答的时序。当每完成一个字节的数据传输以后,就会开启应答来确认一下是否接收到了数据。其中,在主机接收应答前,需要把SDA的电平拉下来,然后从机如果接收到了数据,需要立马将SDA回弹至高电平,只有这样才算接收到了数据。
这些就是IIC通信的基本单元,下面是一个完整的IIC发送流程:
- 主机先发送开始信号。
- 主机继续发送选择从机的地址码加上发送还是接收选择,其中,地址码通常是一个7位的二进制数据,然后模式选择是选择发送还是接收,其中0表示写,1表示读,拼接起来就是一个8位一个字节的数据。
- 然后就是发送数据,发送一个字节的数据,从机依次接收数据。(这个数据可以是需要写入的地址或者写入的数据,这个具体看从机的要求)
- 从机发送ack表示已经接收到了数据。
- 最后主机可以选择发送stop终止信号。
下面是一个完整的IIC接收流程:
- 主机先发送开始信号。
- 一样的主机发送从机的地址码加一个0。
- 发送你要读取的寄存器的地址,从机会应答。
- 重复起始条件。
- 发送从机地址加1,切换到读的模式。
- 从机控制SDA线,主机读取SDA线以获取数据,如果需要继续发送,主机发送ACK就行了,如果终止发送,主机发送NACK。
- 最后主机发送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" #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" #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" #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的初始化也是通过构造结构体并且传入数值来进行设置。

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

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

这是接收器的时序图,写代码按照这个图上的流程进行基本的配置就可以了,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" #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() {
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通信的原理,本质上发送和接收是一体的,同时发送同时接收,就是通过一位一位的移,差不多是一个循环模式。


这是最常用的模式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" 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); myspi_w_sck(1); 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" #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" #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

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" 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通信有非常完善的数据纠错等等功能,可以很好地处理异常,错误等各种情况。

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

这是can通信的时序,分为标准格式和扩展格式两种,标准格式和扩展格式的区别是扩展格式的ID可以更长一点,扩展格式id有29位。
不过平常还是使用标准格式比较多,毕竟没这么多设备。

可以对着上图的简介表理解时序图,can通信每一次发送最多可以发送8字节。
还有就是can通信的仲裁段,如果碰到两个设备同时准备发送通信,就会进入仲裁阶段,仲裁的依据主要就是id号的大小,id号越小的仲裁获胜的可能性越大,并且标准大于扩展,数据帧大于遥控帧,这里的位数设计非常巧妙,很好地兼容了标准格式和扩展格式两种情况。
下面是一些can总线为我传输不出错更严谨的独特机制。

首先是位填充,为了避免发送大量相同的数据造成误判的可能情况,所以设计了位填充的机制,就是每发5个相同电平时,会自动在后面追加一个相反电平的填充位,具体机制可以看上图。
这个可以防止发送大量相同的电平,比如连续发送隐性1,被误判为进入了空闲状态,(连续发送11个隐性1就是进入了空闲状态,11位其实就是时序图中的ack界定符加上7的个结束位加上3个的帧间隔)

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

pts是传播时间段,这个通常有硬件问题而配置的,毕竟传输过来还需要时间。
然后就是pbs1和pbs2,这两个就是控制采样位的核心了,也是再同步。

就是通过设置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 = ; }
|
我这里也只是测试了一下回环通信,测试一下软件有没有问题。