基于STM32F103C8T6 HAL库的DMA串口通信与舵机控制实战
1. 项目概述
本项目基于STM32F103C8T6最小系统板,使用HAL库开发,实现以下核心功能:通过DMA接收上位机VOFA+发送的数据,将接收到的数据解析为浮点数,用于驱动舵机旋转到指定角度;同时通过DMA按照JustFloat协议向上位机回传当前角度和PWM占空比数据,实现双向实时通信。
2. 硬件准备
在开始编写代码之前,需要准备以下硬件和软件环境:
- STM32F103C8T6最小系统板:主控芯片,72MHz主频,64KB Flash,20KB RAM。
- 舵机:推荐SG90,工作电压5V,PWM控制信号周期20ms。
- USB转TTL模块:如CH340,用于与上位机通信。
- VOFA+上位机软件:可以下载开源软件VOFA+进行上位机
- 烧录器:可以用stlink或者其他的
- 5V电源:为舵机供电,注意与主控板共地(没有的话,简易版可以用stlink的5V)。
3. JustFloat协议说明
JustFloat协议是VOFA+上位机支持的一种简单浮点数据传输协议,特点是适用于大量浮点数传输,并且实时绘出图像便于调参,VOFA+中简介如下:

可以通过建立一个结构体来存储浮点数数据以及帧尾,帧尾的作用就是让VOFA+能识别到该组数据,每次发送的数据都是数据+帧尾,其中帧尾固定为0x00 0x00 0x80 0x7F
本文介绍代码并不采用结构体,而是通过拷贝浮点数数据到发送包里面,再添加帧尾,可以选择发送的浮点数的个数,并不是固定四个浮点数数据,因为本项目是MCU发送当前角度和占空比,所以需要两个
3. 构思思路
提前做好构思思路,有利于模块化编程,利于后期调试以及方便移植
- 初始化,选择PA10PA9即UART1来进行异步通信,使用PA1即TIM2定时器的通道2来产生PWM,可以选择两个引脚来进行OLED显示方便后期调试
- 定时器的预分频寄存器(PSC)和自动重装寄存器(ARR)配置,要满足周期是20ms,周期公式是((PSC +1) * (ARR + 1)) / 定时器时钟频率,比如介绍代码就是((72 -1 + 1)*(20000 -1 + 1))/ 72M,72M即72*10的6次方,计算得出周期是20ms,占空比 = CCR / ARR,Sg90舵机,0度对应CCR = 500,90度对应CCR = 1500,180度对应CCR = 2500,函数关系式为CCR = Angle / 180 * 2000 + 500
- 编程大体可分为:接受串口发出的的数据 -> 转化为浮点数 -> 计算所需要设置的CCR让舵机旋转 -> 计算出占空比 -> 把两个数据拷贝到发送包里面添加帧尾并发送
3. 开发环境搭建(不会可以去Bi站学习,这一步不难)
使用STM32CubeMX生成初始化代码,配置步骤如下:
- 打开STM32CubeMX,选择芯片STM32F103C8T6。
- 配置时钟树,使用外部8MHz晶振,系统时钟设置为72MHz。
- 配置USART1为异步通信模式,波特率9600,8位数据位,1位停止位,无校验。
- 在USART1的DMA设置中,添加接收和发送两个DMA通道,模式均为Normal。
- 配置TIM2中通道2为PWM输出模式,通道2用于舵机控制,频率50Hz(周期20ms)。
- 生成工程代码,选择MDK-ARM作为工具链。
- 优先级配置

5. 代码实现步骤
按照以下步骤编写代码,实现完整功能:
以下是 fun.c 的完整代码内容:
#include "fun.h"
#define MAXCOUNT 4 //最大通道数,根据自己的需求以及VOFA+的最大通道设置
/**
* @brief 设置定时器2通道2的捕获比较寄存器的值CCR(PA2)
* @param 设置的角度
* @retval 无
**/
void SetTIM2_CCR(float angle)
{
uint32_t CCR = (uint32_t)(angle / 180 * 2000 + 500);
TIM2->CCR2 = CCR;
}
/**
* @brief 获取定时器2通道2的占空比
* @retval 返回占空比
**/
float Get_TIM2Duty(void)
{
return (float)TIM2->CCR2 /TIM2->ARR;
}
bool Rx_flag = 0; //接受标志位
uint8_t Rx_str[20] = {0};
uint16_t Rx_length;
uint8_t Tx_str[MAXCOUNT * 5] = {0};
uint16_t Tx_length = 0;
/**
* @brief 串口通过JustFloat协议发送数据
* @param 传输的浮点数数据指针
* @param 指针的值的个数
* @retval 无
* */
void uart_TransmitJustFloatData(float *data,uint8_t count)
{
Tx_length = count * 4;
memcpy(Tx_str,data,Tx_length); //把data的Tx_length字节数据拷贝到Tx_str中,需包含string.h
Tx_str[Tx_length + 0] = 0x00; //进行帧尾处理,满足VOFA+JustFloat协议要求
Tx_str[Tx_length + 1] = 0x00;
Tx_str[Tx_length + 2] = 0x80;
Tx_str[Tx_length + 3] = 0x7F;
Tx_length += 4;
while(huart1.gState != HAL_UART_STATE_READY); //等待串口1是否空闲,空闲退出
HAL_UART_Transmit_DMA(&huart1, Tx_str, Tx_length);
}
/**
* @brief 接受数据的回调函数
* @param 串口句柄
* @param 接受到的数据大小
* @retval 无
* */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
Rx_length = Size;
Rx_flag = 1;
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,Rx_str,20);
}
}
/**
* @brief 将接受到的字符串数据转化为浮点数数据
* @retval 返回角度
**/
float Get_Angle(void)
{
float Angle = 0;
Angle = atof((char*)Rx_str); //自动将字符串数据转化为浮点数数据,需要包含stdlib.h
Angle = Angle < 0 ? 0 : Angle;
Angle = Angle > 180 ? 180 : Angle;
memset(Rx_str,0,20); //清空Rx_str数据,防止数据错位,需包含string.h
return Angle;
}
以下是 fun.h 的完整代码内容:
#ifndef _FUN_H
#define _FUN_H
#include "main.h"
#include "dma.h"
#include "tim.h"
#include "usart.h"
#include "gpio.h"
#include "stdbool.h"
#include "string.h"
#include "stdlib.h"
#include "OLED.h"
void SetTIM2_CCR(float angle);
float Get_TIM2Duty(void);
void uart_TransmitJustFloatData(float *data,uint8_t count);
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);
float Get_Angle(void);
#endif
以下是 main.c 的部分代码内容:
/* USER CODE BEGIN PV */
extern uint8_t Rx_str[20];
extern bool Rx_flag;
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_TIM2_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
OLED_Init();
OLED_ShowString(1,1,"Ang:");
OLED_ShowString(2,1,"CCR:");
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,Rx_str,20); //开启空闲DMA接受数据
//关掉数据传输到一半是产生中断的功能,默认是开的,不关会丢包
__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
float fdata[2] = {0};
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
static uint32_t lastTick = 0;
uint32_t nowTick = HAL_GetTick();
if (nowTick - lastTick >=500)
{
uart_TransmitJustFloatData(fdata,2);
lastTick = nowTick;
}
if (Rx_flag)
{
fdata[0] = Get_Angle();
SetTIM2_CCR(fdata[0]);
fdata[1] = Get_TIM2Duty();
Rx_flag = 0;
}
OLED_ShowFloat(1,5,fdata[0],3,1);
OLED_ShowFloat(2,5,fdata[1] * 100,2,1);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
6. VOFA+上位机配置
在VOFA+中完成以下配置,即可实现数据可视化与控制:
- 打开VOFA+,选择串口通信模式,设置波特率9600。
- 选择JustFloat协议作为数据解析方式。
- 连接,并点击控件,拖动波形图,鼠标右键选择填充选择第一个,然后Y轴可以选择通道
7. 调试与注意事项
在实际调试过程中,需要注意以下几个关键问题:
- 舵机供电:电流太小舵机会滋滋响
- Rx_str和Tx_str缓冲区要适宜:float为4字节,4个浮点数数据需要4 * 4字节,帧尾一共4字节,所以如果最大通道为4,可设置20字节
- PWM频率或者周期:舵机控制信号频率必须为50Hz,对应周期20ms。
- 占空比范围:舵机0度对应0.5ms高电平,180度对应2.5ms高电平,换算为占空比即2.5%到12.5%。
- 数据丢包:__HAL_DMA_DISABLE_IT(huart1.hdmarx, DMA_IT_HT); 这一句不关闭,数据传输到一半就会产生中断,导致接收数据不全产生丢包现象,HT代表Half Transfer,只要关闭一次即可
- 启动函数:如果接收不到数据或者OLED显示正常数据,但是舵机没有旋转,可以看看main.c文件里面是否开启了DMA空闲接收数据和PWM使能
8. 代码下载链接:
git中的ServoAndJustFloat代码:https://github.com/LCZ-Xuan/basic-project.git
9. 总结
本项目完整实现了基于STM32F103C8T6的DMA串口通信与舵机控制,通过JustFloat协议与VOFA+上位机进行双向数据交互。主要是学习DMA的使用、上位机能力以及JustFloat协议、舵机知识、串口如何处理数据、PSC、ARR定时器知识等。项目虽然小,但体现基础。
更多推荐


所有评论(0)