简介:面向STM32初学者的模拟摇杆ADC读取程序KEIL工程源码,基于STM32F103C8(ARM Cortex-M3内核)设计,实现对摇杆水平、垂直两路模拟信号的采集与解析。工程演示了ADC的完整使用流程:从初始化配置(输入通道、采样时间、分辨率、数据对齐)到启动转换、等待标志、读取数据寄存器,并引入低通滤波和中断驱动方式,帮助理解实时采样与噪声抑制。资源共258个文件,约7.05MB,以.h头文件、.c源文件为主,另含KEIL工程文件(.uvproj)、编译输出(.o、.axf、.hex)以及映射/列表文件(.map、.lst等),除主程序main.c外还包含系统初始化、延迟函数等配套模块,可直接在KEIL中打开工程、阅读源码、编译烧录验证。目前已有195人学习下载,适合STM32入门者深入学习ADC外设、中断系统及代码模块化设计,也可作为毕业设计或嵌入式项目的基础模板。
1. 这个标题背后的真实需求:给模拟摇杆建一条稳定的 ADC 读数通道
如果你在做遥控器、迷你航模地面站或手柄调试工具,第一件让人头疼的事就是拇指推摇杆时,MCU 能不能拿到稳定且可重复的 X / Y 坐标。基于 STM32F103C8 的设计通常会直接采集摇杆两端电位器的分压,但很多人把数据用串口打印出来之后会发现:中心点不是 2048 而在 2036~2100 之间漂,推到最边缘读数够不到 4095,相同角度重复读数有十几位跳动。这其实不是摇杆坏了,而是还没有把“硬件接口 + ADC 采样参数 + 软件滤波/校准”当作一条完整信号链来处理。这篇文章应对的就是这样一个 KEIL 工程源码:它说明一个用 F103C8 读取模拟摇杆的工程从哪里下手,以及拿到源码后怎样读懂、验证和改造成自己的项目。适合刚完成单片机入门、需要做第一个模拟量项目的开发者,也适合已经在调遥控类项目、想确认采样参数和校准方法是否合理的工程师。
2. STM32F103C8 的 ADC 与模拟摇杆接口:引脚映射与信号链设计
2.1 模拟摇杆输出的是什么:两个电位器加一个按键
标准 PS2 手柄摇杆模块有 5 个端子:GND、VCC、X、Y、SW。模块内部是两个 10k 电位器,采用分压形式分别输出 X 和 Y 电压。当摇杆居中时电压约为 VCC / 2,推到两个极限时对应接近 VCC 和 GND。SW 脚是一个轻触开关,表示摇杆是否在垂直方向被按下。
于是 ADC 读取程序要面对三个信号:X 模拟量、Y 模拟量、SW 数字量。PCB 上最简单接法是 VCC 接 3.3V、GND 接 GND,X 和 Y 分别接两个能容忍模拟输入的 GPIO。F103C8 上 PA0 和 PA1 分别对应 ADC1 的通道 0 和通道 1。SW 则接一个普通 GPIO,比如 PB0,并打开内部上拉,否则悬空时电平不稳定,按键扫描会频繁误触发。
2.2 F103C8 的 ADC 资源约束:不是所有引脚都能做模拟输入
STM32F103C8T6 集成两个 12 位 ADC,独立模式下单次转换结果范围是 0~4095。它的模拟输入引脚在 GPIO 复用表里有固定映射。常见可用 PA0~PA7、PB0/PB1 等,但不能把 PA9/PA10 拿来当模拟摇杆输入用。习惯上把 X 和 Y 固定接 PA0/PA1,这样 ADC1_IN0 和 ADC1_IN1 在同一规则通道组里,方便后续用扫描模式一次完成两个通道的读取。
另一个资源约束是 GPIO 必须配置为GPIO_Mode_AIN模拟输入模式,不能复用为推挽或开漏。如果代码里仍然把引脚初始化成GPIO_Mode_IPD或GPIO_Mode_IPU,ADC 读到的会是内部上下拉叠加后的破坏性值:中心点偏移、边缘范围缩小。这是“摇杆读不准”里最常见的人为错误,比采样周期设置问题更容易被忽略。
2.3 ADC采样周期与阻抗匹配:为什么摇杆直接连ADC会读数跳动
F103C8 的 ADC 是逐次逼近型,内部有一个采样电容,采样开关闭合期间需要把外部源信号充到足够精度。摇杆内部电位器阻值通常在 10k 量级,源阻抗过高时采样电容不能在规定时间内完成充电,结果就是读数呈现非线性偏移,或者通道切换后有残留电压,让中心点附近出现“楼梯式”跳变。
常见做法是把 ADC 采样周期调长,使用ADC_SampleTime_55Cycles5甚至239Cycles5。需要明确:采样周期越长,外部阻抗带来的误差越小,但整体转换吞吐率会下降。对于摇杆这种变化频率低于 10Hz 的低速信号,用 55.5 周期已经很稳妥,不需要一味追求最高采样率。
与此同时,硬件上可在 X/Y 引脚各并联一个 0.1uF 电容接地。这个电容和电位器形成一个低通 RC,既能滤掉接触噪声,也给 ADC 每次采样提供局部电荷缓存。注意电容过大会让摇杆动作产生明显滞后,常规取 0.01uF~0.1uF,不要为了滤波而加到大 10uF。
2.4 KEIL 工程里常见的初始化路径
拿到一份“KEIL工程源码.zip”时,先看工程目录里是否带stm32f10x.h、system_stm32f10x.c和启动文件startup_stm32f10x_md.s。C8 属于中等容量,不能误选用startup_stm32f10x_hd.s。工程配置里 Device 型号要选STM32F103C8。如果打开后报出大量未定义编号错误,最常见的不是代码问题,而是 Target 设备没有选对,或者 Pack 版本与工程不匹配。
下面我们以标准外设库 3.5 为例,把初始化、转换、DMA 读取完整写出来。选择标准库是因为网上绝大多数 ADC 摇杆工程都建立在它之上,代码短,运行行为清晰;HAL 库思想类似,但初始化代码会更长。
3. 用 KEIL 搭建最小工程:ADC 多通道读取摇杆坐标
3.1 初始化 GPIO 与 ADC1:标准外设库代码
先给出初始化函数代码块:
#include "stm32f10x.h" /** * @brief 摇杆ADC初始化 * X -> PA0(ADC1_IN0) Y -> PA1(ADC1_IN1) */ void Joystick_ADC_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; // 打开GPIOA与ADC1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE); // ADC时钟 = PCLK2 / 6,72MHz主频下为12MHz RCC_ADCCLKConfig(RCC_PCLK2_Div6); // PA0/PA1模拟输入 GPIO_StructInit(&GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; GPIO_Init(GPIOA, &GPIO_InitStructure); ADC_StructInit(&ADC_InitStructure); ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 多通道扫描 ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件启动 ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; // 本次转换2个通道 ADC_Init(ADC1, &ADC_InitStructure); // 通道排列:第1路PA0,第2路PA1,采样时间55.5周期 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_Cmd(ADC1, ENABLE); // 清零内部校准寄存器并重新校准 ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); }逻辑说明:RCC_PCLK2_Div6让 ADC 工作在 12MHz 以下,满足 F103 最大 14MHz 的限制;ADC_NbrOfChannel=2告诉 ADC 规则序列中要转换两个通道,否则它只转换一个通道就停止。最后两行校准语句基于芯片内部参考,目的是自动消除运算放大器与比较器的偏移电压,必须在使能 ADC 后、首次转换前执行,并等待状态位清空。把GPIO_Mode_AIN写成输入模式是一个很隐蔽的错误,ADC 引脚必须有独立的模拟开关路径,普通数字输入模式会绕过它。
参数配置可以用下面这个表快速核对:
| 参数 | 推荐值 | 用意 |
|---|---|---|
| ADC_SampleTime | 55Cycles5 | 阻抗 10k 电位器下减少采样误差 |
| ADC_Mode | Independent | 只使用 ADC1 |
| ADC_ExternalTrigConv | None | 软件启动,由控制代码决定何时开始 |
| ADC_DataAlign | Right | 结果右对齐,直接当整数使用 |
3.2 最小读取方案:用中断读取两通道结果
在不启用 DMA 之前,有两种读法:阻塞查询和中断读取。阻塞查询在扫描连续模式下很容易踩坑,EOC 标志是每个规则通道转换结束都会置位的,连续模式下两个通道转换很快,查询逻辑一旦没有及时清标志,读到的数据可能错位。我建议调试期直接走中断:
uint16_t joystick_raw[2]; uint8_t channel_index = 0; void ADC1_2_IRQHandler(void) { if (ADC_GetITStatus(ADC1, ADC_IT_EOC) != RESET) { joystick_raw[channel_index] = ADC_GetConversionValue(ADC1); channel_index++; if (channel_index >= 2) { channel_index = 0; } ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); } }逻辑说明:每次 EOC 中断代表一次规则组转换完成,ADC_GetConversionValue返回当前规则组数据寄存器ADC1->DR的值。因为通道顺序在初始化时已经指定为 PA0 然后 PA1,所以第一次中断得到 X,第二次得到 Y,通过channel_index轮转。这段代码需要在Joystick_ADC_Init结尾处加入两行:
ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); NVIC_EnableIRQ(ADC1_2_IRQn);参数说明:ADC1_2_IRQn同时覆盖 ADC1 和 ADC2,即使只使用 ADC1 也要用这个中断号注册。中断服务函数名不能随意改名,否则链接器找不到中断向量入口。
3.3 摇杆按键 SW 的处理
SW 是数字信号,按一下输出低电平。把 PB0 配置成输入上拉,初始化代码如下:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_Init(GPIOB, &GPIO_InitStructure);机械按键存在抖动,直接读电平会在按下和松开瞬间产生多次跳变。常见做法是每 10ms 扫描一次,连续两次读到相同状态才认定有效:
uint8_t Joystick_ButtonScan(void) { static uint8_t last_state = 1; static uint8_t stable_count = 0; uint8_t state = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0); // 1松开,0按下 if (state != last_state) { last_state = state; stable_count = 0; return 0; } if (stable_count < 2) { stable_count++; return 0; } return (state == 0) ? 1 : 0; }逻辑说明:调用此函数前需要先有一个 10ms 的延时控制,main 循环里做Delay_Ms(10)后再调用。stable_count累计到 2 表示连续两轮状态一致,此时才认为按键稳定。这个函数返回的是稳定后的电平状态,而不是边沿触发事件。在一些射击类遥控设备中,你可能需要“按下瞬间”事件,那就要在此基础上做一次“上一轮状态是松开、本轮变成按下”的边沿判断,这里不再展开。
4. ADC 读取中的三大参数:采样周期、滤波函数与三条校准
4.1 采样周期与转换时间的关系
STM32F103C8 的 ADC 时钟最高 14MHz,最常见配置是 72MHz 主频经过 6 分频得到 12MHz。单个通道转换时间 = 采样时间 + 固定 12.5 个周期。以ADC_SampleTime_55Cycles5为例:
- 采样时间 55.5 周期,固定转换 12.5 周期,共 68 周期
- 12MHz 下单通道耗时约 5.67us
- 规则序列中有 2 个通道,一个完整周期约 11.34us
这个速度对摇杆来说完全够用。如果切换到最快采样时间ADC_SampleTime_1Cycles5,单通道耗时约 1.17us,但对 10k 电位器源阻抗极易出现充电不足、线性度变差的问题。下表是常用档位的实测推算:
| 采样周期宏 | 采样周期数 | 单通道总周期 | 双通道序列耗时@12MHz | 适用场景 |
|---|---|---|---|---|
| ADC_SampleTime_1Cycles5 | 1.5 | 14 | 2.33us | 极低阻抗运放输出 |
| ADC_SampleTime_55Cycles5 | 55.5 | 68 | 11.33us | 10k电位器摇杆 |
| ADC_SampleTime_239Cycles5 | 239.5 | 252 | 42us | 高阻传感器或强噪声环境 |
注意:采样周期不是越快越好,源阻抗高时反而应该加长。判断是否需要加长采样时间,可以把摇杆拨到中间,然后快速推到头再回到中间,看串口数据是否出现明显的“拉不过去”现象。如果每次都差一段距离,优先加长采样周期,其次再考虑加并联电容。
4.2 C语言实现简单稳定 ADC 值滤波函数
摇杆信号本身带宽很低,滤波重点是去除机械触点和空间干扰带来的脉冲噪声。纯滑动平均对正态随机噪声有效,但对偶发的尖峰抑制很差;中值滤波能去尖峰,但不平滑。工程里更常用的是“去极值平均”:把最近 N 次采样排序,去掉最大和最小值,再对剩余值求平均。
下面是适合放进 KEIL 工程的滤波函数:
#define ADC_FILTER_LEN 5 static uint16_t filter_buf[2][ADC_FILTER_LEN]; static uint8_t filter_pos[2] = {0, 0}; static uint8_t filter_full[2] = {0, 0}; uint16_t Joystick_Filter(uint16_t raw, uint8_t ch) { uint16_t tmp[ADC_FILTER_LEN]; uint8_t i, j; uint8_t idx = ch; // 写入当前值到环形缓冲区 filter_buf[idx][filter_pos[idx]] = raw; filter_pos[idx] = (filter_pos[idx] + 1) % ADC_FILTER_LEN; if (filter_pos[idx] == 0) { filter_full[idx] = 1; } // 缓冲区未满时直接返回原始值 if (!filter_full[idx]) { return raw; } // 复制到临时数组并排序 for (i = 0; i < ADC_FILTER_LEN; i++) { tmp[i] = filter_buf[idx][i]; } for (i = 0; i < ADC_FILTER_LEN - 1; i++) { for (j = i + 1; j < ADC_FILTER_LEN; j++) { if (tmp[i] > tmp[j]) { uint16_t t = tmp[i]; tmp[i] = tmp[j]; tmp[j] = t; } } } // 去掉最大最小值后求平均 return (uint16_t)((tmp[1] + tmp[2] + tmp[3]) / 3); }逻辑说明:filter_buf[2][ADC_FILTER_LEN]的第一维对应 X/Y 两个通道,因为滤波必须分通道进行,不能把两个轴的采样混在一组里。环形缓冲写入后,如果还没满就返回原始值,这是为了开机初期不产生全零输出。排序用的是冒泡排序,对于长度 5 的数组,开销很小,完全没必要引入快速排序。
参数说明:ADC_FILTER_LEN=5意味着只有连续 5 次扫描才开始滤波,实际延迟约 5×10ms=50ms,肉眼完全察觉不到。如果摇杆松了以后仍然有随机跳动,可以把这个长度改成 7 或 9。改大之后要注意主循环里调用滤波函数的间隔不能太随意,否则不同的调用频率会改变滤波器等效时间常数。
4.3 外部ADC三点校准:解决中心点漂移和量程压缩
摇杆电位器存在个体差异:中心电压不一定是 1.65V,满幅可能只有 3.0V 而不是 3.3V,直接读原始值会导致两个问题:中心点读数漂移、满量程被压缩。校准是工程上最常用的手段,记录一个轴的三个值:最小值raw_min、中心值raw_mid、最大值raw_max,然后做分段线性映射。
先定义校准结构体:
typedef struct { uint16_t raw_min; uint16_t raw_mid; uint16_t raw_max; } Joystick_Calib;校准采样函数需要人为操作摇杆位置,可以把三个位置的读取放到一个状态机里:
void Joystick_CalibRecord(Joystick_Calib *calib, uint8_t ch) { calib[ch].raw_min = Joystick_Filter(joystick_raw[ch], ch); Delay_Ms(20); calib[ch].raw_mid = Joystick_Filter(joystick_raw[ch], ch); Delay_Ms(20); calib[ch].raw_max = Joystick_Filter(joystick_raw[ch], ch); }映射函数把原始 ADC 值映射到-1000 ~ 1000的工程坐标,方便后续 PID 或速度控制使用:
int16_t Joystick_Mapped(uint16_t raw, const Joystick_Calib *calib, int16_t out_min, int16_t out_max) { int32_t mapped; int16_t mid = calib->raw_mid; if (raw <= mid) { if (mid == calib->raw_min) { return 0; } mapped = (int32_t)(raw - calib->raw_min) * (out_max - out_min) / (mid - calib->raw_min) + out_min; } else { if (calib->raw_max == mid) { return 0; } mapped = (int32_t)(raw - mid) * (out_max - out_min) / (calib->raw_max - mid) + out_min; } if (mapped < out_min) mapped = out_min; if (mapped > out_max) mapped = out_max; return (int16_t)mapped; }逻辑说明:raw <= mid走左边区间,否则走右边区间,保证中心点连续。使用int32_t做中间变量是因为(raw - min) * 2000可能超过 16 位整型范围,把乘法溢出算进去是 ADC 数据处理最常见的越界坑。除法前检查分母为 0,否则校准数据异常时整个程序会进入硬件错误中断。
还要注意:ADC 数据漂移不仅仅是电位器的问题,电源电压波动、基准电压不稳定、PCB 地平面回流都会让中心点上下移动。三点校准可以补偿固定偏差和增益误差,但不能补偿随机噪声。所以顺序必须是:先软件滤波,再映射校准,不能在校准前先做映射,那样滤波函数收到的原始值是未校准数据,做去极值平均还行;但如果把映射后的值再送入滤波,会引入非线性问题。
5. 用 DMA 搬运多通道数据:避免主循环被 ADC 卡死
5.1 DMA1 通道配置:连续扫描模式下传输两个通道
中断读取对低速摇杆够用,但会让主循环频繁进中断,数据量大时还会丢 EOC。更好做法是让 ADC 配合 DMA,自动把两个通道的转换结果搬到内存里,缓冲填满后产生一次 DMA 传输完成中断。配置代码在Joystick_ADC_Init之后执行:
#define JOYSTICK_ADC_BUF_SIZE 2 __IO uint16_t joystick_dma_buf[JOYSTICK_ADC_BUF_SIZE]; void Joystick_ADC_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)joystick_dma_buf; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = JOYSTICK_ADC_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }参数说明:DMA_PeripheralBaseAddr必须写(uint32_t)&(ADC1->DR),而不是先读取到一个临时变量再赋值,DMA 每次搬运都需要从外设地址取数。外设数据宽度HalfWord对应 ADC 的 12 位结果,MemoryInc必须打开,否则两个通道的结果都会覆盖到同一个地址。DMA_Mode_Circular让传输完成后自动从缓冲区头重新开始,主循环读取就不需要手动重启 DMA。
在 ADC 初始化函数Joystick_ADC_Init的末尾还需要增加一行使能 ADC 的 DMA 请求:
ADC_DMACmd(ADC1, ENABLE);顺序很重要:必须先ADC_DMACmd,再调用Joystick_ADC_DMA_Init,最后执行ADC_SoftwareStartConvCmd(ADC1, ENABLE)。如果把ADC_DMACmd放在 DMA 初始化之后,DMA 可能因为 ADC 第一次转换已经产生请求而错过首轮。
5.2 DMA 中断与主循环数据同步
DMA 配置成循环模式后,每传输完 2 个半字就触发一次传输完成中断。为了不让主循环到处拼时间片,可以在中断里把数据复制到快照,同时置一个标志:
__IO uint8_t adc_new_data = 0; __IO uint16_t joystick_snapshot[2]; void DMA1_Channel1_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC1) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TC1); joystick_snapshot[0] = joystick_dma_buf[0]; joystick_snapshot[1] = joystick_dma_buf[1]; adc_new_data = 1; } }主循环写法如下:
while (1) { if (adc_new_data) { adc_new_data = 0; joystick_raw[0] = Joystick_Filter(joystick_snapshot[0], 0); joystick_raw[1] = Joystick_Filter(joystick_snapshot[1], 1); // 再调用 Joystick_Mapped 得到工程坐标 } // 其他任务 }逻辑说明:joystick_snapshot采用 volatile 修饰,防止编译器把两次读 DMA 缓冲合并成一次内存访问。在中断里复制两个半字数据开销极小,却能避免主循环在读joystick_dma_buf时 DMA 又更新同一位置,导致高低字节拼错。置位adc_new_data后主循环一次性消费数据,这种“标志位 + 快照”模式比直接共享数组更安全。
如果你以后要接四通道摇杆或者多个滑杆,思路一样:把JOYSTICK_ADC_BUF_SIZE改成 4,在 ADC 初始化里加通道,再通过DMA_BufferSize=4让 DMA 一次搬完。建议把快照数组也改成同尺寸,保持一对一转对应关系。
5.3 KEIL 工程里容易踩的三个编译/运行坑
第一个坑:编译报error: No section matches selector - no section to be FIRST/LAST,多半是启动文件选错。把startup_stm32f10x_hd.s用在 F103C8 上会报类似错误,C8 应该使用startup_stm32f10x_md.s。启动文件是汇编源文件,决定了中断向量表首位放置方式,它和芯片容量系列必须匹配。
第二个坑:Keil uVision5 提示设备不匹配。打开Options for Target -> Device,在 STMicroelectronics 目录下选择STM32F103C8,确认 Flash 大小为 64KB。很多从 F103RB 项目复制过来的工程,Device 依然指向 F103RB,编译器会按更大 Flash 规划分散加载文件,烧录后行为异常。
第三个坑:Error: L6218E: Undefined symbol Delay_Ms,标准外设库工程没有自带延时函数,代码里有调用但工程里没有实现。要么自己写一个 SysTick 延时函数,要么把校准代码里的Delay_Ms(20)去掉。Keil 安装 Pack 时如果出现“pack install 硬件错误”,通常是安装包来源不完整,重新下载稳定版本,不要用工程自带的旧 Pack 覆盖当前版本。
6. 收尾:用串口输出摇杆轨迹验证线性度
工程跑起来后,要验证的不是“能不能读到数字”,而是摇杆坐标是否覆盖完整区域、中心是否居内。最简单的方法是用 USART1 把映射后的 X/Y 值持续发出来,在 PC 串口终端里观察。发送代码可以这样写:
int16_t map_x = Joystick_Mapped(joystick_raw[0], &calib[0], -1000, 1000); int16_t map_y = Joystick_Mapped(joystick_raw[1], &calib[1], -1000, 1000); char msg[32]; sprintf(msg, "%4d,%4d\r\n", map_x, map_y); USART_SendString(USART1, msg);验证时让用户手握摇杆画圈,串口终端里的坐标输出应该围绕(0,0)旋转,边缘不超过±1000。如果图形是椭圆且中心明显偏向右下,说明两点校准没做对,或者 X/Y 通道接反;如果图形边缘出现“削峰”,说明raw_max采集点没有真正推到极限,需要重新校准。把摇杆固定在一个位置不动,打印原始值 1000 次,计算最大值减最小值的差;差值小于 3 说明信号链已经稳定,差值为 10 甚至更高时,要优先检查采样周期是否太短以及 PCB 上 ADC 引脚旁边是否漏掉了 0.1uF 电容。
没有串口模块时,也可以把映射后的坐标转换成 PWM 占空比,驱动两个 LED,推摇杆时观察 LED 亮度是否从暗到亮均匀变化。这个可视化方法不依赖上位机,在调试现场很直接:把map_x加 1000 后除以 2,得到 0~1000 的占空比数值,赋值给定时器比较寄存器,即可用眼睛评估线性度,而不需要盯着波形。
本文还有配套的精品资源,点击获取