>应用案例>
/N32G031_SERVO_POTENTIOMETER

使用电位器控制舵机

N32G031_SERVO_POTENTIOMETER

简介

这是一个交互式自动化项目,使用 Nations N32G031(Cortex-M0)讲解模拟信号读取与硬件数值映射。项目展示如何通过芯片内部 ADC 读取电位器(旋钮),实时控制舵机位置,并已针对 UnityMbed 跨平台工作流完成优化。

目标 MCUNations N32G031 (Cortex-M0)
模拟与 PWM 引脚PA0 (ADC) + PA1 (Servo)
滤波引擎1D Kalman Filter (防抖)

演示视频

接线与硬件连接

设备器件引脚N32G031 开发板引脚备注与安全提示
电位器(模拟旋钮)信号PA0模拟输入(ADC 通道 0)
电位器(模拟旋钮)VCC3.3V / 5V旋钮电源
电位器(模拟旋钮)GNDGND系统共地
舵机(例如 SG90)信号(橙色/黄色)PA1控制信号引脚(软件模拟 PWM)
舵机(例如 SG90)VCC (红色)5V电机主电源(需要稳定的 5V)
舵机(例如 SG90)GND (棕色/黑色)GND系统共地

运行方式与执行逻辑

微控制器读取电位器的模拟电压,使用卡尔曼滤波器过滤噪声,并实时跟踪舵机位置:

旋钮向左转

移向 180°(脉冲:5500)

当 ADC 读数接近 0 时,舵机会平滑移向 180°(脉冲:5500)。

旋钮向右转

移向 0°(脉冲:2250)

当 ADC 读数接近 4095 时,舵机会平滑移向 0°(脉冲:2250)。

实时跟踪

同步旋钮位置

舵机臂会持续、即时地跟随电位器的准确位置,并保持平稳。

main.c
// UnityMbed — 使用电位器(ADC)实时控制舵机 @ N32G031
#include "n32g031.h"
#include "n32g031_gpio.h"
#include "n32g031_rcc.h"
#include "n32g031_adc.h"
#include "n32g031_usart.h"
#include <stdio.h>

/* ====================================================================
 * [硬件配置区]
 * ==================================================================== */
#define PULSE_MIN     2250
#define PULSE_MAX     5500
#define KNOB_MAX      4095

/* 简单的软件延时循环 */
void Delay_Loop(volatile uint32_t count) {
  while(count--) { __NOP(); }
}

/* 用于抑制传感器噪声的卡尔曼滤波函数(一维简化卡尔曼滤波) */
float kalman_update(float measurement, float* state, float* pc) {
  float k_gain = *pc / (*pc + 0.1f);     // 0.1 为 R(测量噪声)
  *pc = (1.0f - k_gain) * (*pc) + 0.01f; // 0.01 为 Q(过程噪声)
  *state = *state + k_gain * (measurement - *state);
  return *state;
}

/* 初始化 PA0 上的电位器 ADC(ADC 通道 0) */
void Setup_Knob(void) {
  RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA, ENABLE);
  RCC_EnableAHBPeriphClk(RCC_AHB_PERIPH_ADC, ENABLE);

  GPIO_InitType GPIO_InitStructure;
  GPIO_InitStruct(&GPIO_InitStructure);
  GPIO_InitStructure.Pin = GPIO_PIN_0;
  GPIO_InitStructure.GPIO_Mode = GPIO_MODE_ANALOG;
  GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

  ADC_InitType ADC_InitStructure;
  ADC_InitStruct(&ADC_InitStructure);
  ADC_InitStructure.MultiChEn      = DISABLE;
  ADC_InitStructure.ContinueConvEn = DISABLE;
  ADC_InitStructure.ExtTrigSelect  = ADC_EXT_TRIGCONV_NONE;
  ADC_InitStructure.DatAlign       = ADC_DAT_ALIGN_R;
  ADC_InitStructure.ChsNumber      = 1;
  ADC_Init(ADC, &ADC_InitStructure);
  ADC_Enable(ADC, ENABLE);
}

/* 将 PA9 配置为 USART1 TX,用于向串口监视器发送数据 */
void Setup_UART(void) {
  RCC_EnableAPB2PeriphClk(RCC_APB2_PERIPH_GPIOA | RCC_APB2_PERIPH_USART1, ENABLE);

  GPIO_InitType GPIO_InitStructure;
  GPIO_InitStruct(&GPIO_InitStructure);
  GPIO_InitStructure.Pin            = GPIO_PIN_9;
  GPIO_InitStructure.GPIO_Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStructure.GPIO_Alternate = GPIO_AF4_USART1;
  GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

  USART_InitType USART_InitStructure;
  USART_InitStructure.BaudRate            = 115200;
  USART_InitStructure.WordLength          = USART_WL_8B;
  USART_InitStructure.StopBits            = USART_STPB_1;
  USART_InitStructure.Parity              = USART_PE_NO;
  USART_InitStructure.HardwareFlowControl = USART_HFCTRL_NONE;
  USART_InitStructure.Mode                = USART_MODE_TX;
  USART_Init(USART1, &USART_InitStructure);

  USART_Enable(USART1, ENABLE);
}

/* 读取电位器旋钮的模拟值(返回 0 至 4095) */
uint32_t Read_Knob(void) {
  ADC_ConfigRegularChannel(ADC, ADC_CH_0, 1, ADC_SAMP_TIME_56CYCLES5);
  ADC_EnableSoftwareStartConv(ADC, ENABLE);
  while(ADC_GetFlagStatus(ADC, ADC_FLAG_ENDC) == RESET);
  ADC_ClearFlag(ADC, ADC_FLAG_ENDC);
  return ADC_GetDat(ADC);
}

/* 通过 UART 逐字符发送字符串 */
void UART_SendString(const char* str) {
  while(*str) {
      USART_SendData(USART1, (uint8_t)*str++);
      while (USART_GetFlagStatus(USART1, USART_FLAG_TXDE) == RESET);
  }
}

/* 向舵机发送控制信号脉冲 */
void Servo_Step(uint32_t pulse_width) {
  GPIO_SetBits(GPIOA, GPIO_PIN_1);
  Delay_Loop(pulse_width);
  GPIO_ResetBits(GPIOA, GPIO_PIN_1);
  Delay_Loop(100000);
}

/* ====================================================================
 * 主应用循环
 * ==================================================================== */
int main(void) {
  /* --- 1. 系统初始化 --- */
  Setup_UART();
  UART_SendString("\r\n[DEBUG] UART 初始化完成,接线正确。\r\n");

  Setup_Knob();
  UART_SendString("[DEBUG] ADC 初始化完成,正在读取基准值……\r\n");

  // 将舵机控制引脚 PA1 初始化为推挽输出
  GPIO_InitType GPIO_InitStructure;
  GPIO_InitStruct(&GPIO_InitStructure);
  GPIO_InitStructure.Pin = GPIO_PIN_1;
  GPIO_InitStructure.GPIO_Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitPeripheral(GPIOA, &GPIO_InitStructure);

  /* --- 2. 变量设置 --- */
  uint32_t raw_knob = 0;
  uint32_t target_pulse = PULSE_MIN;

  // 读取初始基准值,防止舵机启动时抖动
  float k_state = (float)Read_Knob();
  float k_pc = 0.0f;

  char msg_buffer[64];
  uint32_t print_counter = 0; // 用于限制 UART 输出频率

  UART_SendString("系统初始化完成,开始控制舵机……\r\n");

  /* --- 3. 主控制循环 --- */
  while(1) {
      // 步骤 A:读取电位器的原始模拟值
      raw_knob = Read_Knob();

      // 步骤 B:使用卡尔曼滤波器消除电气噪声和抖动
      kalman_update((float)raw_knob, &k_state, &k_pc);

      // 步骤 C:将滤波后的 ADC 值(0–4095)映射为舵机脉宽(2250–5500)
      // 注意:此处使用反向计算(PULSE_MAX - ...),以匹配旋钮的实际转动方向
      target_pulse = PULSE_MAX - (((uint32_t)k_state * (PULSE_MAX - PULSE_MIN)) / KNOB_MAX);

      // 步骤 D:发送计算出的脉冲以驱动舵机
      Servo_Step(target_pulse);

      // 步骤 E:向串口监视器输出遥测数据(每 50 次循环输出一次,避免信息过多)
      if (++print_counter >= 50) {
          sprintf(msg_buffer, "原始 ADC: %4lu | 滤波后: %4d | 脉冲: %4lu\r\n",
                  raw_knob, (int)k_state, target_pulse);
          UART_SendString(msg_buffer);
          print_counter = 0; // 重置计数器
      }
  }
}

学习与 AI 扩展思路

学习者可以使用 IDE 内置的 AI Assistant 探索、调试和修改本项目。可以尝试粘贴以下示例提示词:

1

理解数值映射公式

提示词: “请说明反向公式 PULSE_MAX - ((knob_value * (PULSE_MAX - PULSE_MIN)) / KNOB_MAX) 如何将 12 位 ADC 读数转换为正确的舵机角度。”

2

实验移动平均滤波

提示词: “如何修改 Read_Knob() 函数,对 10 次读数取平均值,从而让手保持不动时舵机不再抖动?”

3

排查接线问题

提示词: “ADC 总是返回 4095 或 0。PA0 上的电位器接线可能哪里有问题?”

4

探索卡尔曼滤波器

让 AI 说明 kalman_update 函数中的一维卡尔曼滤波器,与简单移动平均滤波相比,如何动态消除测量抖动。

构建与烧录指南

1

打开项目: 直接在 IDE 中打开此项目文件夹。

2

Build & Flash: 只需点击界面工具栏上的 Build 和 Flash 按钮。