STM32F103C8T6摇杆方向控制完整例程:ADC采集、死区判定与串口输出

发布时间:2026/9/8 11:00:24
STM32F103C8T6摇杆方向控制完整例程:ADC采集、死区判定与串口输出 简介面向STM32F103C8T6的摇杆模块与串口模块联合控制方向示例工程适合嵌入式单片机开发初学者或需要快速搭建方向控制功能的项目开发者。工程代码框架完整预留了添加自定义逻辑的特定代码位置可在此基础上实现摇杆坐标采集、串口指令发送与方向判定便于扩展为手柄、智能小车等实际应用。软件包共614个文件以C源文件、H头文件为主并配有启动文件、链接脚本、IAR与Keil工程文件以及AXF/HEX/MAP等编译烧录与调试辅助文件同时集成STM32 HAL库和部分CMSIS DSP库代码容量约18.17MB目录结构清晰可直接打开编译。已有1560人学习下载代码使用中若有具体问题作者提供私聊交流支持。 做小车、机械臂、云台这类项目第一步基本都是把“方向控制”跑通。而 STM32F103C8T6 最小系统板搭配一个 PS2 摇杆模块Joystick、一个串口模块是我实践中验证过最快、也最适合新手学习和二次开发的组合。这篇内容就是围绕这套硬件给出完整的代码例程摇杆 ADC 采集、方向判定逻辑、串口数据输出以及预留的功能扩展点位——你在对应的位置加上电机驱动、舵机控制或上位机协议就能直接接入自己的项目。适合谁看正在做循迹小车、遥控小车、舵机云台或者刚开始接触 STM32 开发、想搞明白“摇杆数值怎么读、方向怎么判、串口怎么发”的人。文中贴出的例程基于 HAL 库 CubeMX代码结构清楚改一改就能用。1. 项目整体设计与思路拆解1.1 这套组合能干什么、适合什么场景先说清楚这套方案的本质摇杆是一个模拟量输入设备STM32 通过 ADC模数转换器读取摇杆在 X 轴和 Y 轴上的电压偏移再通过串口把方向数据发送给上位机或者其他主控板。最典型的应用场景有两类调试验证类你还没写电机驱动先想确认“摇杆读数对不对、方向判断准不准、串口收不收得到”这套代码直接把方向结果打在串口助手上5 分钟就能验证。模块预研类你最终要做的是遥控小车、水下机器人、云台相机方向盘读取和串口上报是公共底层功能先把这部分做稳后续业务代码直接在预留位置扩展不用回头改底层。我自己的习惯是“先打通数据链路再写业务逻辑”。摇杆方向数据如果能稳定地出现在串口助手上说明 ADC 采集、方向算法、串口发送三个环节都没有问题后面接电机、接舵机就只剩逻辑问题而不是硬件问题。1.2 为什么选 STM32F103C8T6 摇杆 串口STM32F103C8T6 几乎是国内嵌入式入门最常用的一颗芯片Cortex-M3 内核主频 72MHz20KB RAM、64KB Flash片上自带的 12 位 ADC 和多个 UART 完全够用。重点是小系统板价格便宜坏了不心疼而且网上资料极多遇到问题很容易搜到答案。摇杆模块用的是最常见的 PS2 手柄摇杆模块内部就是两个电位器加一个按键开关X 轴和 Y 轴各输出一路电压信号按下摇杆时输出一个开关量。这类模块大量用在 Arduino 项目中电路简单也完全兼容 STM32。串口选择 CH340 方案的 USB 转 TTL 模块主要是因为驱动成熟、兼容性好。Windows、Linux、macOS 都有驱动串口助手打开直接能看到数据是非常稳妥的调试通道。整个方案没有复杂外设没有外部中断依赖只需要基本的 ADC 采样和 UART 发送非常适合作为入门级方向控制模板。1.3 整体架构模块划分与代码组织结构我把整个程序拆成三层这样后续扩展最省事硬件驱动层负责 ADC、UART、按键引脚初始化通过 CubeMX 生成业务逻辑层负责读取摇杆 ADC 值、做方向判定、生成方向字符串应用扩展层预留的代码点位你后续加的电机控制、协议解析、状态机全部放在这里。代码里每一层都有明确的注释和函数边界主循环里只保留最核心的调用关系不堆业务。这样做的好处是如果以后要换摇杆模块接不同的引脚只改硬件驱动层如果要改方向判定算法只动业务逻辑层而不管你怎么改应用扩展层都不受影响。2. 硬件连接与底层配置2.1 摇杆模块的工作原理摇杆模块的本质是两路电位器分压电路。摇杆水平方向移动时X 轴电位器的动片位置变化、输出电压变化垂直方向移动时Y 轴电位器同理。当摇杆处于中心位置时两个输出脚电压大约是电源电压的一半3.3V 供电时约为 1.65V。STM32 的 ADC 是 12 位分辨率满量程对应数值范围是 04095。也就是说摇杆中心读数大约在 2048 附近向一个方向推到底接近 0 或 4095。模块还有一个 SW 脚按下摇杆时输出低电平平时是高电平可以当作一个普通按键使用。这里有一个很容易踩的坑不同厂家生产的摇杆模块中心点电压并不严格等于 1.65V可能存在几百毫伏的偏差对应到 ADC 数值上就是一两百的偏移。所以方向判定不能直接用 2048 作为唯一中心点我会在后面的死区处理里重点说明。2.2 引脚分配与接线表我使用的是 STM32F103C8T6 最小系统板引脚分配如下模块引脚STM32引脚对应功能摇杆 GNDGND电源地摇杆 VCC3.3V电源正摇杆 VRxPA0ADC1_IN0X轴摇杆 VRyPA1ADC1_IN1Y轴摇杆 SWPB0普通输入按键串口 TXPA9USART1_TX串口 RXPA10USART1_RX串口 GNDGND共地接线注意事项串口模块 TTL 电平需要和 STM32 的 3.3V 逻辑兼容CH340 模块一般都有 3.3V 电平模式如果模块上有跳线帽确认设置在 3.3V 或 TTL 位置串口 TX 接模块 RX串口 RX 接模块 TX交叉连接一定共地。我见过很多人串口收不到数据最后发现是 GND 没连。2.3 CubeMX 基础配置我用的是 STM32CubeMX HAL 库下面几步是必须配置的SYS 里的 Debug 设置为 Serial Wire防止第一次烧录后调试口被占用将 PA0、PA1 配置为 ADC1 的 IN0、IN1 两个通道选择扫描模式Scan Mode Enabled分辨率 12 位关闭连续转换因为我在主循环里用轮询方式读取USART1 设置为异步模式Asynchronous波特率 1152008 位数据无校验1 位停止位PB0 配置为 GPIO_Input同时把上拉电阻打开Pull-up保证摇杆 SW 悬空时是高电平。代码生成选择 MDK-ARMToolchain 选自己的编译器版本。CubeMX 生成完成后打开工程接下来先在main.c里补上 printf 重定向这样后续可以直接用printf打印调试信息省去每次调用串口发送函数的麻烦。2.4 串口重定向与初始化验证在串口调试阶段用printf比直接调HAL_UART_Transmit方便得多。重定向代码如下#include stdio.h int fputc(int ch, FILE *f) { HAL_UART_Transmit(huart1, (uint8_t *)ch, 1, 0xFFFF); return ch; }加完这段printf(Hello\r\n)就能直接通过 USART1 发送。注意 Windows 串口助手如果显示乱码优先检查波特率是否和代码一致。初始化部分我做了一个基础验证函数上电后通过串口打印一行系统启动信息同时打印摇杆 X/Y 轴的原始 ADC 值和按键状态。这行日志不是摆设它能帮你快速确认硬件接线、ADC 配置和串口链路是否全部正常是整套例程的“自检灯”。3. 核心逻辑方向判定算法与代码实现3.1 方向判定的核心思路ADC 原始值到方向离散值摇杆 ADC 读出来是 04095 的连续数值直接发给串口没有意义用户或者上位机需要的是“上、下、左、右、左前、右后”这样离散的方向结果。因此核心思路是把模拟量映射成状态量。我采用的判定方法是X 轴和 Y 轴各自独立判断成 3 种状态再组合成 9 种方向结果X 轴状态左L、中C、右RY 轴状态上U、中C、下D组合结果就是上、下、左、右、左上、右上、左下、右下、中间。如果摇杆按键按下则优先上报按键事件。这样做的好处是每一轴逻辑独立代码可读性强后续想扩展灵敏度参数也很方便。3.2 死区处理与阈值计算ADC 中心理论值是 2048但实际回中位置会漂移且机械摇杆在使用过程中会有抖动如果直接把阈值卡死在 2048摇杆放在中心位置时方向会在“左/右”之间反复跳变噪声非常大。所以必须引入死区以中心值为基准左右各留一段不判向的范围。我实测自己这块板子的摇杆回中值在 19802100 之间波动因此设定了两个阈值#define AXIS_MIN 1700 // 小于该值判断为低左/下 #define AXIS_MAX 2300 // 大于该值判断为高右/上也就是说ADC 值落在 [1700, 2300] 区间内都算“中间状态”。死区宽度是 600对大多数摇杆模块来说足够稳定同时又不会让方向触发显得迟钝。如果你发现摇杆推一点方向就有反应但回中时还是偶尔跳方向就把这个区间再调大一点反过来如果觉得推动行程很大才有反应就适当调小。这里顺便说一下方向符号约定不同模块 X/Y 轴的正负方向可能和标注相反以实际测试为准。比如我这块板子把摇杆向右推X 轴 ADC 值增大向下推Y 轴 ADC 值增大。如果你的模块相反只需把状态判断里的比较方向对调即可。3.3 完整代码实现与预留点位下面是完整的核心代码在 CubeMX 生成的工程上替换 main 函数即可。代码里已经预留了功能扩展位置。/* 引脚与 ADC 相关 */ ADC_HandleTypeDef hadc1; UART_HandleTypeDef huart1; #define AXIS_MIN 1700 #define AXIS_MAX 2300 /* 摇杆按键状态 */ #define JOYSTICK_SW_PRESSED (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) GPIO_PIN_RESET) /* X 轴和 Y 轴的状态枚举 */ typedef enum { AXIS_STATE_LOW 0, AXIS_STATE_CENTER, AXIS_STATE_HIGH } AxisState_t; /* 根据 ADC 值判断轴状态 */ AxisState_t get_axis_state(uint16_t adc_value) { if (adc_value AXIS_MIN) { return AXIS_STATE_LOW; } else if (adc_value AXIS_MAX) { return AXIS_STATE_HIGH; } return AXIS_STATE_CENTER; } /* 读取摇杆 ADC 值 */ void joystick_read(uint16_t *x_value, uint16_t *y_value) { /* 启动 ADC 转换 */ HAL_ADC_Start(hadc1); /* 第一次转换对应 Rank1 - ADC_IN0 (PA0, X轴) */ HAL_ADC_PollForConversion(hadc1, 10); *x_value HAL_ADC_GetValue(hadc1); /* 第二次转换对应 Rank2 - ADC_IN1 (PA1, Y轴) */ HAL_ADC_PollForConversion(hadc1, 10); *y_value HAL_ADC_GetValue(hadc1); /* 停止 ADC */ HAL_ADC_Stop(hadc1); } /* 生成方向字符串 */ void get_direction_str(uint16_t x_value, uint16_t y_value, char *buf, uint8_t buf_len) { AxisState_t x_state get_axis_state(x_value); AxisState_t y_state get_axis_state(y_value); const char *x_str C; const char *y_str C; if (x_state AXIS_STATE_LOW) x_str L; if (x_state AXIS_STATE_HIGH) x_str R; if (y_state AXIS_STATE_LOW) y_str U; /* 注意这里按实际方向调整 */ if (y_state AXIS_STATE_HIGH) y_str D; /* 组合方向 */ if (strcmp(x_str, C) 0 strcmp(y_str, C) 0) { snprintf(buf, buf_len, CENTER); } else { snprintf(buf, buf_len, %s%s, x_str, y_str); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_ADC1_Init(); MX_USART1_UART_Init(); uint16_t x_val 0, y_val 0; char dir_buf[16]; printf(System Boot OK\r\n); while (1) { /* 读取摇杆原始值 */ joystick_read(x_val, y_val); /* 按键按下时优先处理按键事件 */ if (JOYSTICK_SW_PRESSED) { snprintf(dir_buf, sizeof(dir_buf), PRESS); } else { /* 生成方向字符串 */ get_direction_str(x_val, y_val, dir_buf, sizeof(dir_buf)); } /* 通过串口输出方向结果和原始数值 */ printf(Dir: %-6s X: %4d Y: %4d\r\n, dir_buf, x_val, y_val); /* */ /* 在这里添加你想要的功能代码 */ /* */ /* 比如根据 dir_buf 控制电机 / 舵机 / 上报协议 */ /* 比如根据 x_val、y_val 做云台速度闭环 */ /* */ HAL_Delay(50); /* 50ms 采样周期25ms 可能太灵敏看实际情况 */ } }关键点解释joystick_read函数里两次HAL_ADC_PollForConversion对应两个通道的转换结果顺序由 CubeMX 里的 Rank 顺序决定我配置的 Rank1 是 IN0、Rank2 是 IN1所以顺序不能写反方向字符串选择了简化命名L左、R右、U上、D下组合如LU左上、RD右下printf里的%-6s是为了让方向字符串在串口助手里对齐显示看着方便主循环里的HAL_Delay(50)是采样周期50ms 对方向控制来说足够了不会影响实时性也不至于刷屏太快导致上位机处理不过来。4. 实操验证与常见问题排查4.1 串口调试完整步骤烧录完成后打开串口调试助手按以下顺序验证选择正确的串口号。在电脑设备管理器里看端口COM编号CH340 通常会显示 “USB-SERIAL CH340 (COMx)”设置波特率 115200数据位 8停止位 1无校验这与代码配置保持一致打开串口后如果一切正常首先会收到一行System Boot OK不动摇杆时屏幕上会持续刷新Dir: CENTER X: 约2000 Y: 约2000这样的数据推动摇杆方向字符串会变成L、R、U、D或组合方向同时 X/Y 数值也会相应变化按下摇杆会打印PRESS。如果第 3 步就卡住了说明串口链路本身有问题优先检查接线和驱动如果第 4 步中心数值偏差很大比如读到 3000 多优先检查摇杆 VCC 是否接了 3.3V以及电位器模块是否是坏的。4.2 常见问题速查表现象可能原因解决方向串口助手收不到任何数据串口 RX/TX 接反、GND 未共地、没装 CH340 驱动交叉检查接线重装驱动后查看设备管理器收到数据但全是乱码波特率不一致、时钟配置错误确认两边波特率一致检查 CubeMX 里 HSE 晶振频率是否填对只收到一次System Boot OK后无输出主循环卡死、HAL_Delay 配置异常检查是否在while(1)里死循环仿真器单步调试定位摇杆放中间方向偶尔跳变死区太小、中心值偏了调大AXIS_MIN和AXIS_MAX的差距实测打印中心值再定ADC 数值跳动厉害供电不稳、接线过长、ADC 未滤波加滤波或者做几次滑动平均缩短杜邦线距离按下按键没反应SW 引脚没有内部上拉 / 模块无上拉电阻把 PB0 配置为 Pull-up或者外接 10k 上拉电阻Hal 库串口中断只收一次接收中断未重新开启 / 没清标志位这个例程以发送为主如果后面做接收每收一帧后重新调用接收函数4.3 我踩过的几个坑做这个例程时有几次调试经历印象很深写出来供大家参考。第一是关于 ADC 顺序的坑。第一次用 CubeMX 配置双通道 ADC 时我以为HAL_ADC_GetValue会自动返回指定通道的值结果发现两个通道读出来的数据是反的。原因就是 Rank 顺序决定了转换顺序必须按 Rank1、Rank2 的顺序读取不能想当然。这也是我在代码里加了详细注释的原因。第二是死区参数。我最初用的是基于 2048 的对称死区范围设得非常窄结果摇杆回中时方向信息一直在 CENTER 和 LEFT 之间来回跳。后来我把串口读到的基础值打出来花了十几分钟盯着一组数据看偏移情况才确定合理阈值。这里强烈建议拿到一个摇杆模块先写一个打印原始 ADC 值的测试程序把中心值、四向推满的值都记录下来再定死区参数效果比直接套别人的参数好得多。第三是串口地线的问题。有次在项目板上调试怎么都收不到数据波形也看不清折腾半小时后才发现 USB 转串口模块和 STM32 板子各自接了不同的电源GND 没有连在一起。嵌入式调试里“共地”这个事说一百遍都不嫌多。5. 预留扩展与后续方向代码里已经预留了非常明确的扩展点在/* 在这里添加你想要的功能代码 */下面你可以加任何内容。这里给几个我实际做过的扩展思路接电机驱动根据dir_buf控制 L298N 或 TB6612 的 IN 引脚和 PWM 输出实现摇杆控制小车前进、后退、转向接舵机云台把 X 轴 ADC 值映射到舵机角度实现摇杆控制云台转动。映射公式大致是angle (adc_value / 4095.0) * 180.0需要注意限幅和回差接上位机协议把当前代码里的printf输出换成自定义协议比如帧头 长度 类型 数据 校验和这样可以和 PC 端软件、手机 App 对接做速度控制不用方向离散量直接根据摇杆偏离中心的幅度计算目标速度也就是把 X/Y 模拟量映射成 PWM 占空比实现渐进式调速。这套代码虽然看着简单但已经包含了“采集 — 算法 — 输出 — 扩展”完整的一套工程思路在此基础上做项目会比直接抄一份电机控制代码理解深得多。最后再分享一个小技巧调方向阈值参数时别靠猜直接在串口助手打印原始 X/Y 值然后用一个纸笔记录摇杆在中心、上下左右尽头时的读数。把读数写下来再确定代码里的阈值比反复改代码下载快得多。我现在做所有摇杆类项目都会先跑一遍原始值打印例程把这个习惯保留下来能省掉大量调参时间。本文还有配套的精品资源点击获取