我想做一个网站怎么办,wordpress保存为模板,电子商务公司网站模版,公司网页设计推广服务公司目录 
代码#xff1a; 
注释写的较少#xff0c;但本文出现的代码都有注释#xff0c;所以请直接在本文里看注释 
项目概述#xff1a; 
一 准备阶段#xff08;都是些废话#xff09; 二 裸机测试功能 
1.摇杆控制 
接线#xff1a; 
CubeMX配置#xff1a; 
代码 
2…目录 
代码 
注释写的较少但本文出现的代码都有注释所以请直接在本文里看注释 
项目概述 
一 准备阶段都是些废话 二 裸机测试功能 
1.摇杆控制 
接线 
CubeMX配置 
代码 
2.蓝牙控制 
接线 
CubeMX配置 
代码 
3.示教器控制 
4.记录动作信息 
5.执行记录的动作 
注 
三 FreeRTOS上完成项目 
1.加入IIC的OLED屏显 和 动作执行数组版  
CubeMX配置 代码 
2.链表的增删遍历实现动作记忆和执行 
3.SPI扩容 
功能测试 
w25q128芯片分析与功能实现 代码 
注释写的较少但本文出现的代码都有注释所以请直接在本文里看注释 链接https://pan.baidu.com/s/14GJF8ZCnkNkKkz5R0uJwOA?pwd1111  提取码1111  --来自百度网盘超级会员V4的分享 项目概述 基于STM32的FreeRTOS四轴机械臂 基于FreeRTOS实时操作系统主控为 STM32F103C8T6 机械臂为四轴分别被四个Mg90s舵机控制。本项目实现了 3 种控制方法分别为 摇杆控制  、 串口蓝牙控制 和 示教器控制。可以进行动作录制和执行。 采用8路ADC采集摇杆和示教器的模拟量并由DMA搬运数据USART串口实时收发信息IIC驱动OLED屏幕实时显示信息。并且实现了动作录制和执行功能动作记忆可以由二维数组或者链表实现存储。通过SPI驱动W25Q128模块进行动作记忆扩容即可以录制上百组动作。 
一 准备阶段都是些废话 首先你需要一台四轴机械臂才能开始这个项目。可以自己建模3D打印也可以直接某宝购买了一套成品套件来做功能实现。而你的机械臂会配备四个电机本文采用的是舵机型号无所谓控制起来是一样的注意需要是180度的角度型舵机而不是360度的速度型舵机。 然后是单片机及开发环境使用STM32F103C8T6。开发环境为STM32cubeM和Keil5。 
如果你没有STM32开发经验首先你至少要有一点C语言基础最基本的代码要能读懂什么意思然后最好有过其它单片机开发经验比如C51、ESP8266等等或者直接学习一下STM32开发。板子随便买一个此型号的开发板就行买最小系统板面包板也可以。STM32cubeMXKeil5可以自行百度搜索并下载安装我建议在B站找一个STM32HAL库的教程跟着安装且最好教程芯片型号与你使用的要一致。按照教程走一遍。确认开发板和开发环境可用之后简单学习一下HAL库开发。然后可以继续下面的步骤。 
STM32cubeM和Keil5的教程推荐 【中科大RM电控合集】手把手KeilSTM32CubeMXVsCode环境配置_哔哩哔哩_bilibili 其它硬件准备 
HC系列蓝牙串口模块实测HC-05和HC-08都可以摇杆模块买两个即可。四个旋钮电位器质量别太差。IIC协议OLED屏幕SPI协议W25Q128模块按钮模块若干我用了四个有板载的按钮也可以尽量买带电容的防抖按钮舵机拓展板可有可无面包板也能用。各式杜邦线若干。 二 裸机测试功能 
1.摇杆控制 首先是摇杆控制STM32需要4路1ADCDMA采集摇杆输出的模拟量。根据这个数据来控制舵机角度。蓝牙串口把ADC信息和舵机角度打印出来。蓝牙直接用HC官方的HC蓝牙串口助手就行。 
接线 
摇杆4个输出模拟量的引脚连接stm32的A0A1A2A3VCC这里接5V。 
舵机A夹爪    CH4_B11;adc4_A3 
舵机B上下    CH3_B10;adc3_A2 
舵机C前后    CH2_B3;adc2_A1 
舵机D底座    CH1_A15;adc1_A0 
蓝牙TX对板子RX A10 
蓝牙RX对板子TX A9。 
CubeMX配置 
基本配置后面每个工程都是这一套 ADC14路 DMA搬运ADC数据的 PWM输出选用799*1799这样可以把舵机有效的 0.5~2.5ms / 20ms 这个区间分成180段对应0~180度。 usart9600波特率给蓝牙模块用。 然后 generate code 即可 
代码 
注只有这种注释之间是用户自己写业务代码的地方写其它地方再重生成功能会被清除。 
/* USER CODE BEGIN */。。。。 。。。。/* USER CODE END */ 
main.c 
关键控制代码在于check的四个函数首先限制舵机的角度范围避免损坏再根据采集的摇杆信息值判断每个舵机的角度是增加还是减小。 
注释比较清楚直接看代码就行。 
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include stdio.h
/* USER CODE END Includes *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */
uint16_t adc_dma[4];//DMA搬运的ADC采集值uint8_t angle[4]  {90,90,90,90};//舵机角度uint8_t cnt  0;//计数用定时串口打印信息/* USER CODE END PV *//* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
//覆写printf用于串口打印数据
int fputc(int ch, FILE *f)
{      unsigned char temp[1]{ch};HAL_UART_Transmit(huart1,temp,1,0xffff);  return ch;
}//根据输入的0~180角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{return pwm_pulse  44;
}//舵机A夹爪	CH4_B11
void cheack_A()
{if(adc_dma[3]  4000  angle[3]  90){angle[3];}else if(adc_dma[3] 1000  angle[3]  0){angle[3]--;}
}
//舵机B上下	CH3_B10
void cheack_B()
{if(adc_dma[2] 1000  angle[2]  135){angle[2];}else if(adc_dma[2]  4000  angle[2]  45){angle[2]--;}
}
//舵机C前后	CH2_B3
void cheack_C()
{if(adc_dma[1] 1000  angle[1]  135){angle[1];}else if(adc_dma[1]  4000  angle[1]  45){angle[1]--;}
}
//舵机D底座	CH1_A15
void cheack_D()
{if(adc_dma[0] 1000  angle[0]  180){angle[0];}else if(adc_dma[0]  4000  angle[0]  0){angle[0]--;}
}/* USER CODE END 0 *//* USER CODE BEGIN 2 */HAL_ADC_Start_DMA(hadc1,(uint32_t *)adc_dma,4); //开始ADC和DMA采集//开启4路PWMHAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4);//延时半秒系统稳定一下HAL_Delay(500);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 *///根据摇杆DMA判断舵机该如何运动cheack_A();cheack_B();cheack_C();cheack_D();//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));cnt;//计数每循环一次1if(cnt 50)//循环50次每次20ms即一共1s。每一秒发送一次数据{printf(Angle  {%d, %d, %d, %d}\r\n,angle[0],angle[1],angle[2],angle[3]);cnt  0;}HAL_Delay(20);//每20ms循环一次改成15更流畅}/* USER CODE END 3 */ 
这里要勾选才能使用printf串口打印信息  2.蓝牙控制 
这里提前写了一点示教器的业务代码执行切换模式操作会切换获取摇杆模拟值还是电位器模拟值。 
注意我这里整活儿搞了个ADC通道切换但实测还是存在一点问题你们直接使用8通道一起就好。 
接线 
先不使用示教器但是可以先测试一下功能切换模式和采集一下数据。 
把四个旋钮电位器接好四根线接到ADC 5 6 7 8 
CubeMX配置 
打开串口中断中断接收数据。  我这里是ADC再开四个其它不配置。 
你们开启共8个之后把下面个数也4改成8新的组别5678也改成IN4 5 6 7 四个通道 代码 
都写在main.c里面会太冗长我这里分文件编程了不懂可以百度keil怎么添加.c .h文件实在不行就都放在main.c里吧。。。 
adc.c 
纯粹整活儿自定义了一个ADC初始化把采集1234换成5678来采集电位器信号。直接用八个通道一起采集就行然后把原来放采集数据的那个数组adc_dma长度也改成8。 
/* USER CODE BEGIN 1 */
//写一个切换通道的函数
/* ADC1_Mode2 init function */
void MX_ADC1_Mode2_Init(void)
{ADC_ChannelConfTypeDef sConfig  {0};/** Common config*/hadc1.Instance  ADC1;hadc1.Init.ScanConvMode  ADC_SCAN_ENABLE;hadc1.Init.ContinuousConvMode  ENABLE;hadc1.Init.DiscontinuousConvMode  DISABLE;hadc1.Init.ExternalTrigConv  ADC_SOFTWARE_START;hadc1.Init.DataAlign  ADC_DATAALIGN_RIGHT;hadc1.Init.NbrOfConversion  4;if (HAL_ADC_Init(hadc1) ! HAL_OK){Error_Handler();}/** Configure Regular Channel*/sConfig.Channel  ADC_CHANNEL_4;sConfig.Rank  ADC_REGULAR_RANK_1;sConfig.SamplingTime  ADC_SAMPLETIME_1CYCLE_5;if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK){Error_Handler();}/** Configure Regular Channel*/sConfig.Channel  ADC_CHANNEL_5;sConfig.Rank  ADC_REGULAR_RANK_2;if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK){Error_Handler();}/** Configure Regular Channel*/sConfig.Channel  ADC_CHANNEL_6;sConfig.Rank  ADC_REGULAR_RANK_3;if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK){Error_Handler();}/** Configure Regular Channel*/sConfig.Channel  ADC_CHANNEL_7;sConfig.Rank  ADC_REGULAR_RANK_4;if (HAL_ADC_ConfigChannel(hadc1, sConfig) ! HAL_OK){Error_Handler();}
}
/* USER CODE END 1 */ 
adc.h 
/* USER CODE BEGIN Prototypes */
void MX_ADC1_Mode2_Init(void);
/* USER CODE END Prototypes */ 
usart.c 
注STM32串口接收到的信息都在这里进行处理,千万别忘了最下面一行代码开启中断。 //中断信息处理。。。。。。。。        // 
/* USER CODE BEGIN 0 */
#include stdio.h
#include string.h
#include PWM.h#include adc.h
#include dma.h/*机械臂控制模式默认为1
1摇杆控制	
2示教器控制	*/
uint8_t Mode  1;/*蓝牙控制机械臂指令
s		停
l/r 左右
u/d 上下
f/b 前后
o/c 开合*/
uint8_t cmd_BLE  s;extern uint16_t adc_dma[4];//DMA搬运的ADC采集值//覆写printf
int fputc(int ch, FILE *f)
{      unsigned char temp[1]{ch};HAL_UART_Transmit(huart1,temp,1,0xffff);  return ch;
}//串口中断
//串口接收缓存1字节
uint8_t buf0;
//定义最大接收字节数 200可根据需求调整
#define UART1_REC_LEN 200
// 接收缓冲, 串口接收到的数据放在这个数组里最大UART1_REC_LEN个字节
uint8_t UART1_RX_Buffer[UART1_REC_LEN];
//  接收状态
//  bit15      接收完成标志
//  bit14      接收到0x0d
//  bit13~0    接收到的有效字节数目
uint16_t UART1_RX_STA0;// 串口中断接收完成回调函数收到一个数据后在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{// 判断中断是由哪个串口触发的if(huart-Instance  USART1){// 判断接收是否完成UART1_RX_STA bit15 位是否为1if((UART1_RX_STA  0x8000)  0){// 如果已经收到了 0x0d 回车if(UART1_RX_STA  0x4000){// 则接着判断是否收到 0x0a 换行if(buf  0x0a){// 如果 0x0a 和 0x0d 都收到则将 bit15 位置为1UART1_RX_STA | 0x8000;//中断信息处理//模式切换if (!strcmp((const char *)UART1_RX_Buffer, M1)) {Mode  1;HAL_ADC_Stop_DMA(hadc1);//停止ADC DMAMX_ADC1_Init();//初始化ADC1HAL_ADC_Start_DMA(hadc1,(uint32_t *)adc_dma,4); //开启ADC DMAprintf(摇杆模式\r\n);}else if(!strcmp((const char *)UART1_RX_Buffer, M2)) {Mode  2;HAL_ADC_Stop_DMA(hadc1);//停止ADC DMAMX_ADC1_Mode2_Init();//自定义初始化ADC1把1234换成5678采集电位器HAL_ADC_Start_DMA(hadc1,(uint32_t *)adc_dma,4); //开启ADC DMAprintf(示教器模式\r\n);}//获取蓝牙控制指令A打头后面一个字母就是指令内容else if(Mode  1  UART1_RX_Buffer[0]  A){cmd_BLE  UART1_RX_Buffer[1];}else {if(UART1_RX_Buffer[0] ! \0)printf(指令发送错误%s\r\n, UART1_RX_Buffer);}//memset(UART1_RX_Buffer, 0, strlen((const char *)UART1_RX_Buffer));// 重新开始下一次接收UART1_RX_STA  0;//}else// 否则认为接收错误重新开始UART1_RX_STA  0;}else	// 如果没有收到了 0x0d 回车{//则先判断收到的这个字符是否是 0x0d 回车if(buf  0x0d){// 是的话则将 bit14 位置为1UART1_RX_STA | 0x4000;}else{// 否则将接收到的数据保存在缓存数组里UART1_RX_Buffer[UART1_RX_STA  0X3FFF]  buf;UART1_RX_STA;// 如果接收数据大于UART1_REC_LEN200字节则重新开始接收if(UART1_RX_STA  UART1_REC_LEN - 1)UART1_RX_STA  0;}}}// 重新开启中断HAL_UART_Receive_IT(huart1, buf, 1);}
}/* USER CODE END 0 *//* USER CODE BEGIN USART1_Init 2 */// 开启接收中断HAL_UART_Receive_IT(huart1, buf, 1);/* USER CODE END USART1_Init 2 */ 
我这里新建了两个PWM.c和.h文件。 
把蓝牙指令控制和摇杆控制放在一起判断了。 
#include PWM.h
#include main.hextern uint16_t adc_dma[4];//DMA搬运的ADC采集值
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{return pwm_pulse  44;
}//舵机A夹爪	CH4_B11
void check_A()
{if(Mode  1){if((cmd_BLE  c || adc_dma[3]  4000)  angle[3]  90)//合{angle[3];}else if((cmd_BLE  o || adc_dma[3] 1000)  angle[3]  0)//开{angle[3]--;}}}//舵机B上下	CH3_B10
void check_B()
{if(Mode  1){if((cmd_BLE  u || adc_dma[2] 1000)  angle[2]  135)//上{angle[2];}else if((cmd_BLE  d || adc_dma[2]  4000)  angle[2]  45)//下{angle[2]--;}}}//舵机C前后	CH2_B3
void check_C()
{if(Mode  1){if((cmd_BLE  f || adc_dma[1] 1000)  angle[1]  135)//前{angle[1];}else if((cmd_BLE  b || adc_dma[1]  4000)  angle[1]  45)//后{angle[1]--;}}}
//舵机D底座	CH1_A15
void check_D()
{if(Mode  1){if((cmd_BLE  l || adc_dma[0] 1000)  angle[0]  180)//左{angle[0];}else if((cmd_BLE  r || adc_dma[0]  4000)  angle[0]  0)//右{angle[0]--;}}}#ifndef __PWM_H__
#define __PWM_H__//根据输入的角度获取对应pwm占空比参数
unsigned char Angle(unsigned char pwm_pulse);//舵机A夹爪	CH4_B11
void check_A(void);//舵机B上下	CH3_B10
void check_B(void);//舵机C前后	CH2_B3
void check_C(void);//舵机D底座	CH1_A15
void check_D(void);#endifmain.c  
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include stdio.h
#include PWM.h
/* USER CODE END Includes *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */
uint16_t adc_dma[4];//DMA搬运的ADC采集值uint8_t angle[4]  {90,90,90,90};//舵机角度uint8_t cnt  0;//计数用/* USER CODE END PV *//* USER CODE BEGIN 2 */printf(Start\r\n);HAL_ADC_Start_DMA(hadc1,(uint32_t *)adc_dma,4); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4);HAL_Delay(500);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 *///根据摇杆DMA判断舵机该如何运动check_A();check_B();check_C();check_D();//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));cnt;if(cnt 50){printf(Angle  {%d, %d, %d, %d}\r\n,angle[0],angle[1],angle[2],angle[3]);cnt  0;}HAL_Delay(20);}/* USER CODE END 3 */ 
3.示教器控制 
把示教器控制的业务代码也写出来和蓝牙/摇杆控制封装在一个函数里main里直接调用这个函数就行。 
主要是PWM.c添加了一些代码,直接修改上面代码即可。 
extern uint16_t adc_dma[4];//DMA搬运的ADC采集值直接用8通道就改长度8
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{return pwm_pulse  44;
}
//舵机角度如何变化和模式判断的函数
void sg()
{if(Mode  1)//蓝牙/摇杆模式{check_A();check_B();check_C();check_D();}else if(Mode  2)//示教器模式{translate();}//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));HAL_Delay(20);
}void translate()//把采集的模拟值转变为角度。即0~4095变为0~180除以22.75即可。
{angle[3]  (uint8_t)((double)adc_dma[0] / 22.75)/2;angle[2]  (uint8_t)((double)adc_dma[1] / 22.75);angle[1]  (uint8_t)((double)adc_dma[2] / 22.75) - 10;angle[0]  180 - (uint8_t)((double)adc_dma[3] / 22.75);//电位器装反改为 180 - 即可//直接用8通道就是adc_dma[4~7]
} 
PWM.h 
#ifndef __PWM_H__
#define __PWM_H__//根据输入的角度获取对应pwm占空比参数
unsigned char Angle(unsigned char pwm_pulse);//舵机A夹爪	CH4_B11
void check_A(void);//舵机B上下	CH3_B10
void check_B(void);//舵机C前后	CH2_B3
void check_C(void);//舵机D底座	CH1_A15
void check_D(void);void sg(void);void translate(void);#endif main.c /* USER CODE BEGIN 2 */printf(Start\r\n);HAL_ADC_Start_DMA(hadc1,(uint32_t *)adc_dma,4); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4);HAL_Delay(500);/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */sg();//判断舵机该如何运动cnt;if(cnt 25){printf(Angle  {%d, %d, %d, %d}\r\n,angle[0],angle[1],angle[2],angle[3]);//printf(adc_dma  {%d, %d, %d, %d}\r\n,adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]);cnt  0;}}/* USER CODE END 3 */ 
至此基本的控制功能代码已经完成了。小白的话完成到这里已经很不错了。 
4.记录动作信息 
本质上就是保存当前的舵机的四个角度值。 
这里暂时先用二维数组来做。 
被添加的代码 
蓝牙指令A后的 m g D 对应我们这里 记录当前角度、获取所有记录的角度、删除所有记录。 
#include stdio.h
#include string.huint8_t memory[10][4];//记录用的数组
uint8_t i,j  0;void sg()
{if(Mode  1){check_A();check_B();check_C();check_D();}else if(Mode  2){translate();if(cmd_BLE  m  i9){for(j0;j4;j){memory[i][j]  angle[j];}printf(储存动作\r\n);cmd_BLE  s;i;}else if(cmd_BLE  m  i9)printf(动作已满\r\n);cmd_BLE  s;}if(cmd_BLE  g){for(i0;i10;i){for(j0;j4;j){printf(%d ,memory[i][j]  0x30);}printf(\r\n);if(memory[i][j]  \0)	break;}cmd_BLE  s;}else if(cmd_BLE  D){for(i0;i10;i){memset(memory[i],\0,4);}i  0;printf(已清除动作);cmd_BLE  s;}//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));HAL_Delay(20);
} 
5.执行记录的动作 
这里开始已经转到FreeRTOS上了没继续在裸机上做。所以没写对应源码不过可以拿后面FreeRTOS上实现的代码放在这里。没区别一样可以用。需要你们自己来实现和调试。 
PWM.c 
主要就是下面这两个函数 
location_cnt是数组长度宏定义出来就行自己调整长度 
uint8_t memory[location_cnt][4];
uint8_t i,j  0;uint8_t angle_target[4]  {90,90,90,90};
uint8_t angle_target_flag  0;void get_target()//从数组获得位置信息并转换位角度目标值
{angle_target_flag  0;for(j0;j4;j){if(angle[j]  angle_target[j])	angle_target_flag;}if(angle_target_flag  4)	i;for(j0;j4;j){if(memory[i][j]  \0){i  0;}angle_target[j]  memory[i][j];}
}void reach_target()//角度值像角度目标值靠近用于简单防抖和执行记忆动作
{for(j  0;j 4;j){if(angle[j]  angle_target[j]){angle[j]--;}else if(angle[j]  angle_target[j]){angle[j];}}
}void translate()//根据实际情况做了一点角度矫正和限位
{angle_target[3]  (uint8_t)((double)adc_dma[4] / 22.75)/2;angle_target[2]  (uint8_t)((double)adc_dma[5] / 22.75);angle_target[1]  (uint8_t)((double)adc_dma[6] / 22.75) - 10;angle_target[0]  180 - (uint8_t)((double)adc_dma[7] / 22.75);if(angle_target[1]45)	angle_target[1]45;else if(angle_target[1]135)	angle_target[1]135;if(angle_target[2]45)	angle_target[1]45;else if(angle_target[2]135)	angle_target[1]135;
}//是否记录当前位置信息
void if_BLE_cmd()
{switch(cmd_BLE){case m:if(i  location_cnt){for(j0;j4;j){memory[i][j]  angle[j];}printf(储存动作\r\n);cmd_BLE  s;i;}else{printf(动作已满\r\n);cmd_BLE  s;}break;case g:for(i0;i  location_cnt;i){for(j0;j4;j){printf(%d ,memory[i][j]);}printf(\r\n);if(memory[i][j]  \0)	break;}cmd_BLE  s;break;case D:for(i0; i  location_cnt ;i){memset(memory[i],\0,4);}i  0;printf(已清除动作);cmd_BLE  s;break;}
}void check_sg_cmd()//蓝牙和摇杆控制
{check_A();check_B();check_C();check_D();
} 
usart.c 
/*机械臂控制模式默认为1
1摇杆控制	
2示教器控制
3执行记忆动作
*/
uint8_t Mode  1;//中断信息处理//模式切换if (!strcmp((const char *)UART1_RX_Buffer, M1)) {			Mode  1;printf(摇杆模式\r\n);}else if(!strcmp((const char *)UART1_RX_Buffer, M2)) {			Mode  2;printf(示教模式\r\n);}else if(!strcmp((const char *)UART1_RX_Buffer, M3)) {			Mode  3;printf(执行记忆动作\r\n);} 
freertos.c内相关代码        
和main.c的while循环一样理解就行一样用 /* Infinite loop */for(;;){if(Mode  1)//摇杆和蓝牙控制{check_sg_cmd();}else if(Mode  2)//示教器控制{translate();reach_target();}else if(Mode  3)//动作执行{get_target();reach_target();}if_BLE_cmd();//蓝牙指令处理//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));osDelay(15);//通过调整此延时可以改变机械臂运行速度} 
注 
裸机开发到这里就结束了大部分功能都简单实现出来了。 
如果发现舵机运动每秒顿一次请把每秒串口打印信息关掉就行这是裸机的劣势之一所在。 
三 FreeRTOS上完成项目 
1.加入IIC的OLED屏显 和 动作执行数组版  
下面是移植到FreeRTOS操作系统上运行没法介绍太详细建议先系统学一下STM32 HAL开发以及FreeRTOS再进行。 
快速简述就是把需求分给多个任务去执行 
一个任务负责角度信息处理 
一个任务负责串口发送数据 
一个任务负责OLED屏显 
且这里开始把adc改成直接测8组了 
接线把OLED的SDA和SCL对应板子接好  CubeMX配置 
打开IIC 设置4个外部中断把原来的 ADC IN1~7  IN2~9,因为我板载两个按钮位于A0和A1 
然后B4 B5我自己外接了两个按钮。中断都是下降沿触发 FreeRTOS配置使用了V1版本创建三个任务优先级都设为普通 打开定时器用于两个外部中断按钮B4 B5的定时器消抖我这两个没有硬件防抖如果你们的按钮有电容就不用。 使用单次定时器  代码 
usart.c 
/* USER CODE BEGIN 0 */
#include stdio.h
#include string.h
#include PWM.h#include adc.h
#include dma.h/*机械臂控制模式默认为1
1摇杆控制	
2示教器控制
3执行记忆动作
*/
uint8_t Mode  1;/*蓝牙控制机械臂指令
s/m	停/储存当前动作
l/r 左右
u/d 上下
f/b 前后
o/c 开合*/
uint8_t cmd_BLE  s;extern uint16_t adc_dma[8];//DMA搬运的ADC采集值
extern uint8_t angle[4];uint8_t k;//覆写printf
int fputc(int ch, FILE *f)
{      unsigned char temp[1]{ch};HAL_UART_Transmit(huart1,temp,1,0xffff);  return ch;
}//串口中断
//串口接收缓存1字节
uint8_t buf0;
//定义最大接收字节数 200可根据需求调整
#define UART1_REC_LEN 200
// 接收缓冲, 串口接收到的数据放在这个数组里最大UART1_REC_LEN个字节
uint8_t UART1_RX_Buffer[UART1_REC_LEN];
//  接收状态
//  bit15      接收完成标志
//  bit14      接收到0x0d
//  bit13~0    接收到的有效字节数目
uint16_t UART1_RX_STA0;// 串口中断接收完成回调函数收到一个数据后在这里处理
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{// 判断中断是由哪个串口触发的if(huart-Instance  USART1){// 判断接收是否完成UART1_RX_STA bit15 位是否为1if((UART1_RX_STA  0x8000)  0){// 如果已经收到了 0x0d 回车if(UART1_RX_STA  0x4000){// 则接着判断是否收到 0x0a 换行if(buf  0x0a){// 如果 0x0a 和 0x0d 都收到则将 bit15 位置为1UART1_RX_STA | 0x8000;//中断信息处理//模式切换if (!strcmp((const char *)UART1_RX_Buffer, M1)) {			Mode  1;printf(摇杆模式\r\n);}else if(!strcmp((const char *)UART1_RX_Buffer, M2)) {			Mode  2;printf(示教模式\r\n);}else if(!strcmp((const char *)UART1_RX_Buffer, M3)) {			Mode  3;printf(执行记忆动作\r\n);}//获取蓝牙控制指令else if(UART1_RX_Buffer[0]  A){cmd_BLE  UART1_RX_Buffer[1];}else {if(UART1_RX_Buffer[0] ! \0)printf(指令发送错误%s\r\n, UART1_RX_Buffer);}//memset(UART1_RX_Buffer, 0, strlen((const char *)UART1_RX_Buffer));// 重新开始下一次接收UART1_RX_STA  0;//}else// 否则认为接收错误重新开始UART1_RX_STA  0;}else	// 如果没有收到了 0x0d 回车{//则先判断收到的这个字符是否是 0x0d 回车if(buf  0x0d){// 是的话则将 bit14 位置为1UART1_RX_STA | 0x4000;}else{// 否则将接收到的数据保存在缓存数组里UART1_RX_Buffer[UART1_RX_STA  0X3FFF]  buf;UART1_RX_STA;// 如果接收数据大于UART1_REC_LEN200字节则重新开始接收if(UART1_RX_STA  UART1_REC_LEN - 1)UART1_RX_STA  0;}}}// 重新开启中断HAL_UART_Receive_IT(huart1, buf, 1);}
}/* USER CODE END 0 *//* USER CODE BEGIN USART1_Init 2 */HAL_UART_Receive_IT(huart1, buf, 1);/* USER CODE END USART1_Init 2 */PWM.c        解释注释在前面说过 
#include PWM.h
#include main.h
#include tim.h
#include stdio.h
#include string.hextern uint16_t adc_dma[8];//DMA搬运的ADC采集值
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;uint8_t memory[location_cnt][4];
uint8_t i,j  0;uint8_t angle_target[4]  {90,90,90,90};
uint8_t angle_target_flag  0;//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{return pwm_pulse  44;
}void get_target()
{angle_target_flag  0;for(j0;j4;j){if(angle[j]  angle_target[j])	angle_target_flag;}if(angle_target_flag  4)	i;for(j0;j4;j){if(memory[i][j]  \0){i  0;}angle_target[j]  memory[i][j];}
}void reach_target()
{for(j  0;j 4;j){if(angle[j]  angle_target[j]){angle[j]--;}else if(angle[j]  angle_target[j]){angle[j];}}
}void translate()//根据实际情况做了一点角度矫正和限位
{angle_target[3]  (uint8_t)((double)adc_dma[4] / 22.75)/2;angle_target[2]  (uint8_t)((double)adc_dma[5] / 22.75);angle_target[1]  (uint8_t)((double)adc_dma[6] / 22.75) - 10;angle_target[0]  180 - (uint8_t)((double)adc_dma[7] / 22.75);if(angle_target[1]45)	angle_target[1]45;else if(angle_target[1]135)	angle_target[1]135;if(angle_target[2]45)	angle_target[1]45;else if(angle_target[2]135)	angle_target[1]135;
}//是否记录当前位置信息
void if_BLE_cmd()
{switch(cmd_BLE){case m:if(i  location_cnt){for(j0;j4;j){memory[i][j]  angle[j];}printf(储存动作\r\n);cmd_BLE  s;i;}else{printf(动作已满\r\n);cmd_BLE  s;}break;case g:for(i0;i  location_cnt;i){for(j0;j4;j){printf(%d ,memory[i][j]);}printf(\r\n);if(memory[i][j]  \0)	break;}cmd_BLE  s;break;case D:for(i0; i  location_cnt ;i){memset(memory[i],\0,4);}i  0;printf(已清除动作);cmd_BLE  s;break;}
}void check_sg_cmd()
{check_A();check_B();check_C();check_D();
}//舵机A夹爪	CH4_B11-D1;adc4_A3
void check_A()
{if(Mode  1){if((cmd_BLE  c || adc_dma[3]  4000)  angle[3]  90)//合{angle[3];}else if((cmd_BLE  o || adc_dma[3] 1000)  angle[3]  0)//开{angle[3]--;}}}//舵机B上下	CH3_B10-D2;adc3_A2
void check_B()
{if(Mode  1){if((cmd_BLE  u || adc_dma[2] 1000)  angle[2]  135)//上{angle[2];}else if((cmd_BLE  d || adc_dma[2]  4000)  angle[2]  45)//下{angle[2]--;}}}//舵机C前后	CH2_B3-D3;adc2_A1
void check_C()
{if(Mode  1){if((cmd_BLE  f || adc_dma[1] 1000)  angle[1]  135)//前{angle[1];}else if((cmd_BLE  b || adc_dma[1]  4000)  angle[1]  45)//后{angle[1]--;}}}
//舵机D底座	CH1_A15-D0;adc1_A0
void check_D()
{if(Mode  1){if((cmd_BLE  l || adc_dma[0] 1000)  angle[0]  180)//左{angle[0];}else if((cmd_BLE  r || adc_dma[0]  4000)  angle[0]  0)//右{angle[0]--;}}}PWM.h 
#ifndef __PWM_H__
#define __PWM_H__#define location_cnt 20//根据输入的角度获取对应pwm占空比参数
unsigned char Angle(unsigned char pwm_pulse);//舵机A夹爪	CH4_B11
void check_A(void);//舵机B上下	CH3_B10
void check_B(void);//舵机C前后	CH2_B3
void check_C(void);//舵机D底座	CH1_A15
void check_D(void);void check_sg_cmd(void);void if_BLE_cmd(void);void translate(void);void get_target(void);void reach_target(void);#endif 
OLED.c 
#include OLED.h
#include i2c.h
#include oledfont.hvoid Oled_Write_Cmd(uint8_t OLED_cmd)
{HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, OLED_cmd, 1, 0xff);
}void Oled_Write_Data(uint8_t OLED_data)
{HAL_I2C_Mem_Write(hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, OLED_data, 1, 0xff);
}//OLED初始化代码直接复制粘贴
void Oled_Init(void){Oled_Write_Cmd(0xAE);//--display offOled_Write_Cmd(0x00);//---set low column addressOled_Write_Cmd(0x10);//---set high column addressOled_Write_Cmd(0x40);//--set start line addressOled_Write_Cmd(0xB0);//--set page addressOled_Write_Cmd(0x81); // contract controlOled_Write_Cmd(0xFF);//--128Oled_Write_Cmd(0xA1);//set segment remapOled_Write_Cmd(0xA6);//--normal / reverseOled_Write_Cmd(0xA8);//--set multiplex ratio(1 to 64)Oled_Write_Cmd(0x3F);//--1/32 dutyOled_Write_Cmd(0xC8);//Com scan directionOled_Write_Cmd(0xD3);//-set display offsetOled_Write_Cmd(0x00);//Oled_Write_Cmd(0xD5);//set osc divisionOled_Write_Cmd(0x80);//Oled_Write_Cmd(0xD8);//set area color mode offOled_Write_Cmd(0x05);//Oled_Write_Cmd(0xD9);//Set Pre-Charge PeriodOled_Write_Cmd(0xF1);//Oled_Write_Cmd(0xDA);//set com pin configuartionOled_Write_Cmd(0x12);//Oled_Write_Cmd(0xDB);//set VcomhOled_Write_Cmd(0x30);//Oled_Write_Cmd(0x8D);//set charge pump enableOled_Write_Cmd(0x14);//Oled_Write_Cmd(0xAF);//--turn on oled panel
}void Oled_Clear()
{unsigned char i,j; //-128 --- 127for(i0;i8;i){Oled_Write_Cmd(0xB0  i);//page0--page7//每个page从0列Oled_Write_Cmd(0x00);Oled_Write_Cmd(0x10);//0到127列依次写入0每写入数据列地址自动偏移for(j  0;j128;j){Oled_Write_Data(0);}}
}//在屏幕上显示
//官方提供的代码
void Oled_Show_Char(char row,char col,uint8_t oledChar){ //row*2-2unsigned int  i;Oled_Write_Cmd(0xb0(row*2-2));                           //page 0Oled_Write_Cmd(0x00(col0x0f));                          //lowOled_Write_Cmd(0x10(col4));                            //high	for(i((oledChar-32)*16);i((oledChar-32)*168);i){Oled_Write_Data(F8X16[i]);                            //写数据oledTable1}Oled_Write_Cmd(0xb0(row*2-1));                           //page 1Oled_Write_Cmd(0x00(col0x0f));                          //lowOled_Write_Cmd(0x10(col4));                            //highfor(i((oledChar-32)*168);i((oledChar-32)*1688);i){Oled_Write_Data(F8X16[i]);                            //写数据oledTable1}		
}/******************************************************************************/
// 函数名称Oled_Show_Char 
// 输入参数oledChar 
// 输出参数无 
// 函数功能OLED显示单个字符
/******************************************************************************/
void Oled_Show_Str(char row,char col,char *str){while(*str!0){Oled_Show_Char(row,col,*str);str;col  8;	}		
}OLED.h 
#ifndef __OLED_H__
#define __OLED_H__void Oled_Init(void);
void Oled_Clear(void);void Oled_Show_Str(char row,char col,char *str);#endif 
main.c        放了一点初始化配置 
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include stdio.h
/* USER CODE END Includes *//* Private variables ---------------------------------------------------------*//* USER CODE BEGIN PV */uint16_t adc_dma[8];//DMA搬运的ADC采集值uint8_t angle[4]  {90,90,90,90};//舵机角度/* USER CODE END PV *//* USER CODE BEGIN 2 */// 开启接收中断printf(Start\r\n);//程序开始运行HAL_ADC_Start_DMA(hadc1,(uint32_t *)adc_dma,8); //开启ADC和DMA//系统稳定半秒HAL_Delay(500);/* USER CODE END 2 */ 
freeRTOS.c 
uint8_t anti_shake  0;//定时器按钮消抖标志位 
最下面的函数就是外部中断回调函数anti_shake为0才能通过判断一旦进入就将anti_shake置为1避免因为按下的抖动导致按一次触发好几次中断。通过判断之后启动单次定时器800ms定时器到时间后触发定时器中断回调函数在这里再把anti_shake重新置为0。 
其中由于我的板载按键0和1本身有硬件消抖所以不用软件定时器消抖。 
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include stdio.h
#include PWM.h
#include tim.h
#include adc.h
#include OLED.h
/* USER CODE END Includes *//* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
uint8_t anti_shake  0;//定时器按钮消抖标志位extern uint16_t adc_dma[8];//DMA搬运的ADC采集值extern uint8_t angle[4];//舵机角度extern uint8_t Mode;extern uint8_t memory[location_cnt][4];
extern uint8_t i,j;//角度信息字符串
char speedMes[8];  //IIC发送角度数据的字符串缓冲区
char speedMes1[8];
char speedMes2[8];
char speedMes3[8];
char speedMes4[8];
char speedMes5[8];/* USER CODE END Variables *//* USER CODE BEGIN Header_Start_check_angle */
/*** brief  Function implementing the check_angle thread.* param  argument: Not used* retval None*/
/* USER CODE END Header_Start_check_angle */
void Start_check_angle(void const * argument)
{/* USER CODE BEGIN Start_check_angle *///开启4路PWMHAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4);/* Infinite loop */for(;;){if(Mode  1){check_sg_cmd();}else if(Mode  2){translate();reach_target();}else if(Mode  3){get_target();reach_target();}if_BLE_cmd();//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));osDelay(15);//通过调整此延时可以改变机械臂运行速度}/* USER CODE END Start_check_angle */
}/* USER CODE BEGIN Header_Start_usart_show */
/**
* brief Function implementing the usart_show thread.
* param argument: Not used
* retval None
*/
/* USER CODE END Header_Start_usart_show */
void Start_usart_show(void const * argument)
{/* USER CODE BEGIN Start_usart_show *//* Infinite loop */for(;;){printf(Angle  {%d, %d, %d, %d}\r\n,angle[0],angle[1],angle[2],angle[3]);printf(adc_dma1  {%d, %d, %d, %d}\r\n,adc_dma[0],adc_dma[1],adc_dma[2],adc_dma[3]);printf(adc_dma2  {%d, %d, %d, %d}\r\n,adc_dma[4],adc_dma[5],adc_dma[6],adc_dma[7]);printf(\r\n);osDelay(1000);}/* USER CODE END Start_usart_show */
}/* USER CODE BEGIN Header_Start_OLED_Task */
/**
* brief Function implementing the OLED_Task thread.
* param argument: Not used
* retval None
*/
/* USER CODE END Header_Start_OLED_Task */
void Start_OLED_Task(void const * argument)
{/* USER CODE BEGIN Start_OLED_Task */Oled_Init();Oled_Clear();/* Infinite loop */for(;;){//串口数据的字符串拼装speed是格子每个格子1cmsprintf(speedMes,A: %d ,angle[0]);sprintf(speedMes1,B: %d ,angle[1]);sprintf(speedMes2,C: %d ,angle[2]);sprintf(speedMes3,D: %d ,angle[3]);sprintf(speedMes4,Mode %d ,Mode);sprintf(speedMes5,S %d ,i);Oled_Show_Str(1,5,speedMes);Oled_Show_Str(1,69,speedMes1);Oled_Show_Str(2,5,speedMes2);Oled_Show_Str(2,69,speedMes3);Oled_Show_Str(4,0,speedMes4);Oled_Show_Str(4,64,speedMes5);osDelay(500);}/* USER CODE END Start_OLED_Task */
}/* Callback01 function */
void Callback01(void const * argument)
{/* USER CODE BEGIN Callback01 */anti_shake  0;/* USER CODE END Callback01 */
}/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{switch(GPIO_Pin){case GPIO_PIN_0:HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);Mode  1;printf(摇杆模式\r\n);break;case GPIO_PIN_1:HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET);HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);Mode  2;printf(示教模式\r\n);break;case GPIO_PIN_4:if(anti_shake  0){anti_shake  1;osTimerStart(myTimer01Handle,800);if(ilocation_cnt){for(j0;j4;j){memory[i][j]  angle[j];}printf(储存动作\r\n);i;}else if(i9){printf(动作已满\r\n);}}case GPIO_PIN_5:if(anti_shake  0){anti_shake  1;Mode  3;HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET);HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);printf(执行记忆动作\r\n);osTimerStart(myTimer01Handle,800);}}
}
/* USER CODE END Application */ 
2.链表的增删遍历实现动作记忆和执行 
创建 Memary_LinkList.c和.h文件放链表相关代码 
#include Memary_LinkList.h
#include main.h
#include stdio.h
#include stdlib.hstruct Memary//每个链表节点的结构组成
{uint8_t A,B,C,D,cnt;//四个角度和节点的对应编号struct Memary *next;//下一个链表节点的地址
}*head,*tail,*temp;//声明三个指针分别指向链表头尾和一个临时指针void Memary_Init()//初始化链表
{head  (struct Memary *)malloc(sizeof(struct Memary));head - cnt  0;//链表里没记忆信息时编号只有0head - next  NULL;tail  head;temp  head;
}void prinrt_List()//打印整个链表的内容
{temp  head;while(1){printf(%d:{%d,%d,%d,%d}\r\n,temp-cnt,temp-A,temp-B,temp-C,temp-D);if(temp-next ! NULL)	temp  temp-next;else	break;}
}void delete_List()//清空整个链表
{while(head-next ! NULL){temp  head;head  head-next;free(temp);}temp  head;head-cnt  0;
}void addNode(uint8_t angle[4])//记忆动作即新增一个节点
{if(head-cnt ! 0)//编号cnt从1开始从头节点开始存数据{struct Memary *p  (struct Memary *)malloc(sizeof(struct Memary));tail-next  p;p-cnt  tail-cnt;tail  p;}tail-A  angle[0];tail-B  angle[1];tail-C  angle[2];tail-D  angle[3];tail-cnt;temp  tail;
}uint8_t angle_temp[4];//用于提取记忆的角度
uint8_t *p  angle_temp;//用指针传递uint8_t *getNode()//把记忆信息传出去自动循环传整个链表
{angle_temp[0]  temp-A;angle_temp[1]  temp-B;angle_temp[2]  temp-C;angle_temp[3]  temp-D;if(temp-next  NULL)temp  head;elsetemp  temp-next;return p;
}uint8_t sizeof_List()//别管名字看temp在哪用的
{//反应在OLED上就是可以看见当前存储到第几个动作了或者正在执行第一个动作return temp-cnt;
}#ifndef __MEMARY_LINKLIST__
#define __MEMARY_LINKLIST__#include main.hvoid Memary_Init(void);void addNode(uint8_t angle[4]);void prinrt_List(void);void delete_List(void);uint8_t *getNode(void);uint8_t sizeof_List(void);#endif原PWM.c改成了MG90s.c 
#include MG90s.h
#include main.h
#include tim.h
#include stdio.h
#include string.h
#include Memary_LinkList.hextern uint16_t adc_dma[8];//DMA搬运的ADC采集值
extern uint8_t angle[4];//舵机角度
extern uint8_t Mode;
extern uint8_t cmd_BLE;uint8_t i;uint8_t angle_target[4]  {90,90,90,90};
uint8_t *p_angle_target;uint8_t angle_target_flag  4;//默认第一次为4//根据输入的角度获取对应pwm占空比参数
uint8_t Angle(uint8_t pwm_pulse)
{return pwm_pulse  44;
}void getAngleFromMemary()
{	if(angle_target_flag  4){p_angle_target  getNode();for(i  0;i 4;i){angle_target[i]  *(p_angle_target  i);}}angle_target_flag  0;for(i0;i4;i){if(angle[i]  angle_target[i])	angle_target_flag;}
}void reach_target()
{for(i  0;i 4;i){if(angle[i]  angle_target[i]){angle[i]--;}else if(angle[i]  angle_target[i]){angle[i];}}
}void translate()
{angle_target[3]  (uint8_t)((double)adc_dma[4] / 22.75)/2;angle_target[2]  (uint8_t)((double)adc_dma[5] / 22.75);angle_target[1]  (uint8_t)((double)adc_dma[6] / 22.75) - 10;angle_target[0]  180 - (uint8_t)((double)adc_dma[7] / 22.75);if(angle_target[1]45)	angle_target[1]45;else if(angle_target[1]135)	angle_target[1]135;if(angle_target[2]45)	angle_target[1]45;else if(angle_target[2]135)	angle_target[1]135;
}//是否记录当前位置信息
void if_BLE_cmd()
{switch(cmd_BLE){case m:addNode(angle);printf(储存动作\r\n);cmd_BLE  s;break;case g:prinrt_List();cmd_BLE  s;break;case D:delete_List();printf(已清除动作\r\n);cmd_BLE  s;break;}
}void check_sg_cmd()
{check_A();check_B();check_C();check_D();
}//舵机A夹爪	CH4_B11-D1;adc4_A3
void check_A()
{if(Mode  1){if((cmd_BLE  c || adc_dma[3]  4000)  angle[3]  90)//合{angle[3];}else if((cmd_BLE  o || adc_dma[3] 1000)  angle[3]  0)//开{angle[3]--;}}}//舵机B上下	CH3_B10-D2;adc3_A2
void check_B()
{if(Mode  1){if((cmd_BLE  u || adc_dma[2] 1000)  angle[2]  135)//上{angle[2];}else if((cmd_BLE  d || adc_dma[2]  4000)  angle[2]  45)//下{angle[2]--;}}}//舵机C前后	CH2_B3-D3;adc2_A1
void check_C()
{if(Mode  1){if((cmd_BLE  f || adc_dma[1] 1000)  angle[1]  135)//前{angle[1];}else if((cmd_BLE  b || adc_dma[1]  4000)  angle[1]  45)//后{angle[1]--;}}}
//舵机D底座	CH1_A15-D0;adc1_A0
void check_D()
{if(Mode  1){if((cmd_BLE  l || adc_dma[0] 1000)  angle[0]  180)//左{angle[0];}else if((cmd_BLE  r || adc_dma[0]  4000)  angle[0]  0)//右{angle[0]--;}}}FreeRTOS.c要修改的部分 
//                任务1/* USER CODE END Header_Start_check_angle */
void Start_check_angle(void const * argument)
{/* USER CODE BEGIN Start_check_angle */Memary_Init();//开启4路PWMHAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_2);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_3);HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_4);/* Infinite loop */for(;;){if(Mode  1){check_sg_cmd();}else if(Mode  2){translate();reach_target();}else if(Mode  3){getAngleFromMemary();reach_target();}if_BLE_cmd();//输出PWM波使舵机运动__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_1, Angle(angle[0]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_2, Angle(angle[1]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_3, Angle(angle[2]));__HAL_TIM_SetCompare(htim2, TIM_CHANNEL_4, Angle(angle[3]));osDelay(18);//通过调整此延时可以改变机械臂运行速度}/* USER CODE END Start_check_angle */
}//         任务3/* USER CODE END Header_Start_OLED_Task */
void Start_OLED_Task(void const * argument)
{/* USER CODE BEGIN Start_OLED_Task */Oled_Init();Oled_Clear();/* Infinite loop */for(;;){//串口数据的字符串拼装speed是格子每个格子1cmsprintf(speedMes,A: %d ,angle[0]);sprintf(speedMes1,B: %d ,angle[1]);sprintf(speedMes2,C: %d ,angle[2]);sprintf(speedMes3,D: %d ,angle[3]);sprintf(speedMes4,Mode %d ,Mode);sprintf(speedMes5,S %d ,sizeof_List());Oled_Show_Str(1,5,speedMes);Oled_Show_Str(1,69,speedMes1);Oled_Show_Str(2,5,speedMes2);Oled_Show_Str(2,69,speedMes3);Oled_Show_Str(4,0,speedMes4);Oled_Show_Str(4,54,speedMes5);osDelay(500);}/* USER CODE END Start_OLED_Task */
}//         外部中断case GPIO_PIN_4:if(anti_shake  0){anti_shake  1;osTimerStart(myTimer01Handle,800);addNode(angle);printf(储存动作\r\n);} 
到这里位置视频里我实现的内容都可以完成了。 
至于这个链表最大能存多长的动作我是没去实测反正我们玩绝对是够用了。 
3.SPI扩容 不幸的是我板子的SPI2坏了只有SPI1正常所以我没真正去完成这个功能。 
但是这里我提供了完整的存储思路。大家可自己来实现 
功能测试 PB12设为GPIO输出引脚默认设为高电平充当CS口接线就是CS对CSSCK对CLKMOSI对DIMISO对DO 
VCC用3.35v没试过。 在spi.c中添加        比如如果SPI1就是hspi1SPI2就是hspi2别的都不用动 
/* USER CODE BEGIN 1 */
uint8_t spi2_read_write_byte(uint8_t data)
{uint8_t rec_data  0;HAL_SPI_TransmitReceive(hspi2, data, rec_data, 1, 1000);return rec_data;
}
/* USER CODE END 1 */ 
spi.h 
/* USER CODE BEGIN Prototypes */
uint8_t spi2_read_write_byte(uint8_t data);
/* USER CODE END Prototypes */ 
w25q128.h        这里你CS用的哪个GPIO_OUTW25Q128_CS_GPIO就改成对应的。 
#ifndef __W25Q128_H__
#define __W25Q128_H__#include stdint.h/* W25Q128片选引脚定义 */
#define W25Q128_CS_GPIO_PORT           GPIOB
#define W25Q128_CS_GPIO_PIN            GPIO_PIN_12/* W25Q128片选信号 */
#define W25Q128_CS(x)      do{ x ? \HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_SET) : \HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_RESET); \}while(0)/* FLASH芯片列表 */
#define W25Q128     0XEF17          /* W25Q128  芯片ID *//* 指令表 */
#define FLASH_WriteEnable						0x06 
#define FLASH_ReadStatusReg1				0x05 
#define FLASH_ReadData							0x03 
#define FLASH_PageProgram						0x02 
#define FLASH_SectorErase						0x20 
#define FLASH_ChipErase							0xC7 
#define FLASH_ManufactDeviceID			0x90 /* 静态函数 */
static void w25q128_wait_busy(void);               /* 等待空闲 */
static void w25q128_send_address(uint32_t address);/* 发送地址 */
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入page */
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写flash,不带擦除 *//* 普通函数 */
void w25q128_init(void);                   /* 初始化25QXX */
uint16_t w25q128_read_id(void);            /* 读取FLASH ID */
void w25q128_write_enable(void);           /* 写使能 */
uint8_t w25q128_rd_sr1(void);							/* 读取寄存器1的值 */void w25q128_erase_chip(void);             /* 整片擦除 */
void w25q128_erase_sector(uint32_t saddr); /* 扇区擦除 */
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);     /* 读取flash */
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入flash */#endifw25q128.c 
#include w25q128.h
#include spi.h
#include stdio.h/*** brief       初始化W25Q128* param       无* retval      无*/
void w25q128_init(void)
{uint16_t flash_type;spi2_read_write_byte(0xFF); /* 清除DR的作用 */W25Q128_CS(1);flash_type  w25q128_read_id();   /* 读取FLASH ID. */if (flash_type  W25Q128)printf(检测到W25Q128芯片\r\n);elseprintf(读取到的芯片ID为%x\r\n,flash_type);printf(未检测到W25Q128芯片\r\n);
}/*** brief       等待空闲* param       无* retval      无*/
static void w25q128_wait_busy(void)
{while ((w25q128_rd_sr1()  0x01)  0x01);   /* 等待BUSY位清空 */
}/*** brief       读取W25Q128的状态寄存器1的值* param       无* retval      状态寄存器值*/
uint8_t w25q128_rd_sr1(void)
{uint8_t rec_data  0;W25Q128_CS(0);spi2_read_write_byte(FLASH_ReadStatusReg1);     /* 读状态寄存器1 */rec_data  spi2_read_write_byte(0xFF);W25Q128_CS(1);return rec_data;
}/*** brief       W25Q128写使能*   note      将S1寄存器的WEL置位* param       无* retval      无*/
void w25q128_write_enable(void)
{W25Q128_CS(0);spi2_read_write_byte(FLASH_WriteEnable);   /* 发送写使能 */W25Q128_CS(1);
}/*** brief       W25Q128发送地址* param       address : 要发送的地址* retval      无*/
static void w25q128_send_address(uint32_t address)
{spi2_read_write_byte((uint8_t)((address)16));     /* 发送 bit23 ~ bit16 地址 */spi2_read_write_byte((uint8_t)((address)8));      /* 发送 bit15 ~ bit8  地址 */spi2_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
}/*** brief       擦除整个芯片*   note      等待时间超长...* param       无* retval      无*/
void w25q128_erase_chip(void)
{w25q128_write_enable();    /* 写使能 */w25q128_wait_busy();       /* 等待空闲 */W25Q128_CS(0);spi2_read_write_byte(FLASH_ChipErase);  /* 发送读寄存器命令 */ W25Q128_CS(1);w25q128_wait_busy();       /* 等待芯片擦除结束 */
}/*** brief       擦除一个扇区*   note      注意,这里是扇区地址,不是字节地址!!*              擦除一个扇区的最少时间:150ms** param       saddr : 扇区地址 根据实际容量设置* retval      无*/
void w25q128_erase_sector(uint32_t saddr)
{//printf(fe:%x\r\n, saddr);   /* 监视falsh擦除情况,测试用 */saddr * 4096;w25q128_write_enable();        /* 写使能 */w25q128_wait_busy();           /* 等待空闲 */W25Q128_CS(0);spi2_read_write_byte(FLASH_SectorErase);    /* 发送写页命令 */w25q128_send_address(saddr);   /* 发送地址 */W25Q128_CS(1);w25q128_wait_busy();           /* 等待扇区擦除完成 */
}/*** brief       读取芯片ID* param       无* retval      FLASH芯片ID*   note      芯片ID列表见: w25q128.h, 芯片列表部分*/
uint16_t w25q128_read_id(void)
{uint16_t deviceid;W25Q128_CS(0);spi2_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读 ID 命令 */spi2_read_write_byte(0);    /* 写入一个字节 */spi2_read_write_byte(0);spi2_read_write_byte(0);deviceid  spi2_read_write_byte(0xFF)  8;     /* 读取高8位字节 */deviceid | spi2_read_write_byte(0xFF);         /* 读取低8位字节 */W25Q128_CS(1);return deviceid;
}/*** brief       读取SPI FLASH*   note      在指定地址开始读取指定长度的数据* param       pbuf    : 数据存储区* param       addr    : 开始读取的地址(最大32bit)* param       datalen : 要读取的字节数(最大65535)* retval      无*/
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{uint16_t i;W25Q128_CS(0);spi2_read_write_byte(FLASH_ReadData);       /* 发送读取命令 */w25q128_send_address(addr);                /* 发送地址 */for(i0;idatalen;i){pbuf[i]  spi2_read_write_byte(0XFF);   /* 循环读取 */}W25Q128_CS(1);
}/*** brief       SPI在一页(0~65535)内写入少于256个字节的数据*   note      在指定地址开始写入最大256字节的数据* param       pbuf    : 数据存储区* param       addr    : 开始写入的地址(最大32bit)* param       datalen : 要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!* retval      无*/
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{uint16_t i;w25q128_write_enable();    /* 写使能 */W25Q128_CS(0);spi2_read_write_byte(FLASH_PageProgram);    /* 发送写页命令 */w25q128_send_address(addr);                /* 发送地址 */for(i0;idatalen;i){spi2_read_write_byte(pbuf[i]);          /* 循环写入 */}W25Q128_CS(1);w25q128_wait_busy();       /* 等待写入结束 */
}/*** brief       无检验写SPI FLASH*   note      必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!*              具有自动换页功能*              在指定地址开始写入指定长度的数据,但是要确保地址不越界!** param       pbuf    : 数据存储区* param       addr    : 开始写入的地址(最大32bit)* param       datalen : 要写入的字节数(最大65535)* retval      无*/
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{uint16_t pageremain;pageremain  256 - addr % 256;  /* 单页剩余的字节数 */if (datalen  pageremain)      /* 不大于256个字节 */{pageremain  datalen;}while (1){/* 当写入字节比页内剩余地址还少的时候, 一次性写完* 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理*/w25q128_write_page(pbuf, addr, pageremain);if (datalen  pageremain)   /* 写入结束了 */{break;}else     /* datalen  pageremain */{pbuf  pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 */addr  pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */datalen - pageremain;      /* 写入总长度减去已经写入了的字节数 */if (datalen  256)          /* 剩余数据还大于一页,可以一次写一页 */{pageremain  256;       /* 一次可以写入256个字节 */}else     /* 剩余数据小于一页,可以一次写完 */{pageremain  datalen;   /* 不够256个字节了 */}}}
}/*** brief       写SPI FLASH*   note      在指定地址开始写入指定长度的数据 , 该函数带擦除操作!*              SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block*              擦除的最小单位为Sector.** param       pbuf    : 数据存储区* param       addr    : 开始写入的地址(最大32bit)* param       datalen : 要写入的字节数(最大65535)* retval      无*/
uint8_t g_w25q128_buf[4096];   /* 扇区缓存 */void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{uint32_t secpos;uint16_t secoff;uint16_t secremain;uint16_t i;uint8_t *w25q128_buf;w25q128_buf  g_w25q128_buf;secpos  addr / 4096;       /* 扇区地址 */secoff  addr % 4096;       /* 在扇区内的偏移 */secremain  4096 - secoff;  /* 扇区剩余空间大小 *///printf(ad:%X,nb:%X\r\n, addr, datalen); /* 测试用 */if (datalen  secremain){secremain  datalen;    /* 不大于4096个字节 */}while (1){w25q128_read(w25q128_buf, secpos * 4096, 4096);   /* 读出整个扇区的内容 */for (i  0; i  secremain; i)   /* 校验数据 */{if (w25q128_buf[secoff  i] ! 0XFF){break;      /* 需要擦除, 直接退出for循环 */}}if (i  secremain)   /* 需要擦除 */{w25q128_erase_sector(secpos);  /* 擦除这个扇区 */for (i  0; i  secremain; i)   /* 复制 */{w25q128_buf[i  secoff]  pbuf[i];}w25q128_write_nocheck(w25q128_buf, secpos * 4096, 4096);  /* 写入整个扇区 */}else        /* 写已经擦除了的,直接写入扇区剩余区间. */{w25q128_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */}if (datalen  secremain){break;  /* 写入结束了 */}else        /* 写入未结束 */{secpos;               /* 扇区地址增1 */secoff  0;             /* 偏移位置为0 */pbuf  secremain;      /* 指针偏移 */addr  secremain;      /* 写地址偏移 */datalen - secremain;   /* 字节数递减 */if (datalen  4096){secremain  4096;   /* 下一个扇区还是写不完 */}else{secremain  datalen;/* 下一个扇区可以写完了 */}}}
}main函数里用这段代码测试  /* USER CODE BEGIN 2 */w25q128_init();/* 写入测试数据 */sprintf((char *)datatemp, stm32f103c8t6);w25q128_write(datatemp, FLASH_WriteAddress, TEXT_SIZE);printf(数据写入完成\r\n);/* 读出测试数据 */memset(datatemp, 0, TEXT_SIZE);w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE);printf(读出数据%s\r\n, datatemp);/* USER CODE END 2 */ 
运行结果第一句首先一定是都没读取到 w25q128 这个芯片这段判断代码在w25q128_init();里 
w25q128芯片分析与功能实现 
以前在这个文章里讲过  STM32:SPI_我有在好好学习的博客-CSDN博客 总之 整个存储空间的组成是 
256个Block * 16个Sector * 16个Page * 256个字节  共16MB 
而我们代码增删改查储存信息的单位是按照Sector为单位的即一个扇区一个扇区的处理信息 
阅读官方的芯片手册 可知存储地址为六位十六进制刚好对应16MB 
0        0        0        0        0        0 
前两位表示BLOCK16*16 即256个Block 
第三位对应Sector16个 
第四位对应Page16个 
五六位对应16*16 即256个字节 
所以通过设置这个六位的地址我们就可以决定把数据存在哪个扇区 
例如 3FD000 ,就表示第63Block的第13个Sector又因为我们是按照Sector操作的 
所以通过前三位来选择不同的扇区来存数据 
存和读都是字符串形式。写两个函数分别把字符串转化为链表链表转化为字符串即可。 文章转载自: http://www.morning.kxmyj.cn.gov.cn.kxmyj.cn http://www.morning.hdpcn.cn.gov.cn.hdpcn.cn http://www.morning.nyqzz.cn.gov.cn.nyqzz.cn http://www.morning.kqbjy.cn.gov.cn.kqbjy.cn http://www.morning.hblkq.cn.gov.cn.hblkq.cn http://www.morning.srckl.cn.gov.cn.srckl.cn http://www.morning.kkgbs.cn.gov.cn.kkgbs.cn http://www.morning.lokext.com.gov.cn.lokext.com http://www.morning.pcbfl.cn.gov.cn.pcbfl.cn http://www.morning.monstercide.com.gov.cn.monstercide.com http://www.morning.hlnrj.cn.gov.cn.hlnrj.cn http://www.morning.lmdkn.cn.gov.cn.lmdkn.cn http://www.morning.ggfdq.cn.gov.cn.ggfdq.cn http://www.morning.lmtbl.cn.gov.cn.lmtbl.cn http://www.morning.xczyj.cn.gov.cn.xczyj.cn http://www.morning.ldqzz.cn.gov.cn.ldqzz.cn http://www.morning.wnxqf.cn.gov.cn.wnxqf.cn http://www.morning.djlxz.cn.gov.cn.djlxz.cn http://www.morning.mydgr.cn.gov.cn.mydgr.cn http://www.morning.mlffg.cn.gov.cn.mlffg.cn http://www.morning.qpzjh.cn.gov.cn.qpzjh.cn http://www.morning.rlwgn.cn.gov.cn.rlwgn.cn http://www.morning.lbrwm.cn.gov.cn.lbrwm.cn http://www.morning.lmmyl.cn.gov.cn.lmmyl.cn http://www.morning.frnjm.cn.gov.cn.frnjm.cn http://www.morning.tymnr.cn.gov.cn.tymnr.cn http://www.morning.qxljc.cn.gov.cn.qxljc.cn http://www.morning.dzfwb.cn.gov.cn.dzfwb.cn http://www.morning.ngjpt.cn.gov.cn.ngjpt.cn http://www.morning.bhdtx.cn.gov.cn.bhdtx.cn http://www.morning.kljhr.cn.gov.cn.kljhr.cn http://www.morning.kjtdy.cn.gov.cn.kjtdy.cn http://www.morning.qhln.cn.gov.cn.qhln.cn http://www.morning.wnrcj.cn.gov.cn.wnrcj.cn http://www.morning.dmzfz.cn.gov.cn.dmzfz.cn http://www.morning.gryzk.cn.gov.cn.gryzk.cn http://www.morning.stph.cn.gov.cn.stph.cn http://www.morning.ypktc.cn.gov.cn.ypktc.cn http://www.morning.skrh.cn.gov.cn.skrh.cn http://www.morning.qzglh.cn.gov.cn.qzglh.cn http://www.morning.bwzzt.cn.gov.cn.bwzzt.cn http://www.morning.lywcd.cn.gov.cn.lywcd.cn http://www.morning.hzqjgas.com.gov.cn.hzqjgas.com http://www.morning.27asw.cn.gov.cn.27asw.cn http://www.morning.ypnxq.cn.gov.cn.ypnxq.cn http://www.morning.qnzgr.cn.gov.cn.qnzgr.cn http://www.morning.skkln.cn.gov.cn.skkln.cn http://www.morning.youyouling.cn.gov.cn.youyouling.cn http://www.morning.dkgtr.cn.gov.cn.dkgtr.cn http://www.morning.drcnn.cn.gov.cn.drcnn.cn http://www.morning.lsqmb.cn.gov.cn.lsqmb.cn http://www.morning.hgsmz.cn.gov.cn.hgsmz.cn http://www.morning.lpbrp.cn.gov.cn.lpbrp.cn http://www.morning.bdsyu.cn.gov.cn.bdsyu.cn http://www.morning.lstmg.cn.gov.cn.lstmg.cn http://www.morning.rwmft.cn.gov.cn.rwmft.cn http://www.morning.ztnmc.cn.gov.cn.ztnmc.cn http://www.morning.cyfsl.cn.gov.cn.cyfsl.cn http://www.morning.dwzwm.cn.gov.cn.dwzwm.cn http://www.morning.sskns.cn.gov.cn.sskns.cn http://www.morning.xfmzk.cn.gov.cn.xfmzk.cn http://www.morning.nlcw.cn.gov.cn.nlcw.cn http://www.morning.knlbg.cn.gov.cn.knlbg.cn http://www.morning.pqypt.cn.gov.cn.pqypt.cn http://www.morning.rkfh.cn.gov.cn.rkfh.cn http://www.morning.cprls.cn.gov.cn.cprls.cn http://www.morning.yzdth.cn.gov.cn.yzdth.cn http://www.morning.nldsd.cn.gov.cn.nldsd.cn http://www.morning.jmmzt.cn.gov.cn.jmmzt.cn http://www.morning.jnvivi.com.gov.cn.jnvivi.com http://www.morning.pmxw.cn.gov.cn.pmxw.cn http://www.morning.xhsxj.cn.gov.cn.xhsxj.cn http://www.morning.zdkzj.cn.gov.cn.zdkzj.cn http://www.morning.twdkt.cn.gov.cn.twdkt.cn http://www.morning.fdrb.cn.gov.cn.fdrb.cn http://www.morning.ndmbd.cn.gov.cn.ndmbd.cn http://www.morning.mrfr.cn.gov.cn.mrfr.cn http://www.morning.saletj.com.gov.cn.saletj.com http://www.morning.pzwfw.cn.gov.cn.pzwfw.cn http://www.morning.lcplz.cn.gov.cn.lcplz.cn