ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于DSP28335与AD2S1210的SPI角度读取实战

基于DSP28335与AD2S1210的SPI角度读取实战

简介:面向电机控制方向初学者与嵌入式开发者的DSP28335与AD2S1210旋变解码配置资料,聚焦入职初期如何通过SPIA模块完成高精度位置信息采集这一实际问题。内容围绕SPI接口初始化展开,涵盖时钟极性与相位设置、波特率配置、SPI收发函数编写,以及CS、RESET、RD、A0、A1、SAMPLE、WR、SOE等引脚的电平控制,并给出复位流程、读写函数与工作模式切换的具体实现,寄存器配置部分亦有示例可对照。资源包共1个docx文档,约1.79MB,以图文与代码片段结合的方式组织,便于按流程逐步对照调试。已有1054人学习下载,适合需要快速打通DSP28335与AD2S1210通信链路、理解旋变解码时序与采样逻辑的读者参考借鉴。

1. 电机控制里那颗旋变解码芯片,到底怎么用 SPI 把角度读回来

做电机控制的朋友大概率都碰过这样一个场景:板子上焊着一颗 AD2S1210,旁边是 DSP28335,硬件同事把线一接就说“你读一下角度”,然后你打开工程发现 SPI 初始化、GPIO 时序、模式切换、复位延时全得自己啃。这颗芯片不是普通的 ADC,它是旋转变压器专用的解码芯片,输出的是角度和速度,配置靠 SPI,采样靠 SAMPLE 引脚,模式靠 A0/A1 两根线切。项目里这套代码干的事很明确:用 DSP28335 的 SPIA 模块加上一组 GPIO,把 AD2S1210 从复位、配置寄存器到周期性读取位置信息整条链路跑通。适合正在做伺服驱动、云台、关节模组这类需要高精度角度反馈的开发者,尤其是手里已经有 28335 工程、想把这颗解码芯片接进去的人。下面按我拆这份代码的顺序讲,先看整体怎么搭,再落到每一段能抄的初始化与读写函数。

2. 硬件链路与工程结构:SPIA 加 8 根 GPIO 怎么分工

2.1 先看清 AD2S1210 的引脚角色

这颗芯片和 DSP 之间的连接分两类:一类是 SPI 总线本身,另一类是控制与状态引脚。SPI 这边是 SPISIMO、SPISOMI、SPICLK 三根线,负责寄存器读写和数据回传;控制这边是 CS、RESET、RD、WR、A0、A1、SAMPLE、SOE 八根 GPIO,每一根都对应芯片的一个工作状态。理解这套分工是后面写代码不出错的前提,因为很多时序问题不是 SPI 配错了,而是某根控制线的电平在错误的时刻被拉高或拉低。

引脚方向作用代码里的宏
CSDSP 输出片选,低有效CLR_CS / SET_CS
RESETDSP 输出复位,低电平触发CLR_RESET / SET_RESET
RDDSP 输出读使能,串行模式下保持高CLR_RD / SET_RD
WRDSP 输出写使能,低电平期间传地址或数据CLR_WR / SET_WR
A0 / A1DSP 输出工作模式选择AD2S1210_ModeSelect
SAMPLEDSP 输出采样触发CLR_SAMPLE / SET_SAMPLE
SOEDSP 输出串行接口使能,低电平选串行CLR_SOE / SET_SOE

从表里能看出来,真正走 SPI 的只有三根线,其余全是“开关”。我一般会把这几根 GPIO 的方向和初始电平在板级初始化里一次性配好,避免后面每个函数里重复设置。常见做法是把它们封装成宏,像上面那样用 SET_xxx 和 CLR_xxx,读代码时一眼能看出电平变化,比直接写 GpioDataRegs 可读性高很多。

2.2 工程目录与模块划分

这份代码虽然不长,但结构是清晰的,拆开看大致是这么几层:

project/ ├── main.c // 主函数:初始化 + 循环采集 ├── spi.c / spi.h // SPIA 初始化、SPI_Byte 收发 ├── ad2s1210.c / .h // 复位、模式选择、读写、寄存器配置 ├── gpio.c / gpio.h // 控制引脚宏定义与方向配置 └── delay.c / delay.h // us / ms 级延时

这种划分的好处是职责单一:spi.c 只管总线和字节收发,ad2s1210.c 只管芯片协议,gpio.c 只管电平。二次开发时如果要换 SPI 口或者换芯片,改动范围是可控的。我见过把所有这些塞进 main.c 的写法,调起来也能跑,但一旦要移植到另一块板子,基本等于重写。所以哪怕项目小,也建议按这个粒度分文件。

2.3 时钟与波特率的账要先算清

SPI 的波特率不是随便填的,它由 LSPCLK 和 SPIBRR 决定,公式是 LSPCLK/(SPIBRR+1)。代码里 LSPCLK 按 37.5MHz 算,SPIBRR 取 0x12 也就是 18,得到约 1.974MHz。这个值不是拍脑袋来的,要对照 AD2S1210 串行接口能接受的时钟上限,同时留出余量给 PCB 走线和信号完整性。配置之前先确认系统时钟和 LSPCLK 分频,别拿着 150MHz 的主频直接套公式,那样算出来的波特率会差好几倍,现象就是读回来的数据全是 0xFF 或者随机值。

3. SPIA 初始化与字节收发:时序模式选错后面全白搭

3.1 GPIO 与 SPI 寄存器的初始化顺序

初始化这段代码的顺序是有讲究的:先调 InitSpiaGpio() 把 SPI 相关引脚复用成外设功能,再配 SPICCR、SPICTL、SPIBRR,最后释放复位让 SPI 进入工作状态。顺序反了会出现配置不生效的情况,因为 SPI 还在复位态时写某些位是被忽略的。

// 1. 引脚复用为 SPI 功能 InitSpiaGpio(); // 2. 配置字符长度、时钟极性相位、主模式 SpiaRegs.SPICCR.all = 0x07; // 8 位字符,无相位延迟,上升沿发送 SpiaRegs.SPICTL.all = 0x0006; // 主模式,使能发送,禁止中断 // 3. 波特率 = LSPCLK/(SPIBRR+1) = 37.5MHz/19 ≈ 1.974MHz SpiaRegs.SPIBRR = 0x0012; // 4. 自由运行、禁止自测、释放复位 SpiaRegs.SPIPRI.bit.FREE = 1; SpiaRegs.SPICCR.bit.SPILBK = 0; SpiaRegs.SPICCR.bit.SPISWRESET = 1;

SPICCR 的 0x07 拆开看:低四位是字符长度减一,0x07 表示 8 位;bit6 是时钟极性,0 表示上升沿发送、下降沿接收,也就是常说的模式 0。这个选择必须和 AD2S1210 时序图对上,选成模式 1 或 3 的话,芯片会在错误的边沿采样,读回来的角度值会整体偏移甚至完全错乱。SPICTL 的 0x0006 打开主模式和发送使能,中断先关掉,因为这份代码用的是查询方式等标志位,不靠中断。

3.2 SPI_Byte 的查询式收发

字节收发函数是整个通信的地基,写错一个移位就会导致地址和数据错位:

unsigned char SPI_Byte(unsigned char byte) { // 数据写到高 8 位,低 8 位在 16 位寄存器里被忽略 SpiaRegs.SPITXBUF = (byte << 8); // 查询方式等待接收完成标志 while(SpiaRegs.SPISTS.bit.INT_FLAG != 1); // 读回接收寄存器,取低 8 位有效数据 return ((unsigned char)SpiaRegs.SPI); }

这里有两个容易翻车的点。第一,SPITXBUF 是 16 位寄存器,但 SPI 配成 8 位字符,所以数据要左移 8 位放到高字节,直接写 byte 会发出去 0x00。第二,等待的是 INT_FLAG 而不是别的状态位,这个标志在收发完成后置位,查询到之后必须读一次 SPIRXBUF 才能清掉,否则下一次循环会立刻通过,读到的还是旧数据。返回时强转成 unsigned char 取低 8 位,是因为接收寄存器高字节可能是残留值。

提示:如果发现读回的数据恒为 0 或 0xFF,先别怀疑芯片,用示波器看 SPICLK 有没有波形、CS 有没有在传输期间保持低电平,八成是片选或时钟没出来。

3.3 为什么用查询而不是中断

这份代码全程查询,没有用 SPI 中断。在单任务的主循环里采集角度,查询方式足够,逻辑也简单,不会出现中断里再调 SPI 导致的重入问题。但如果你的系统里 SPI 还要跑别的从设备,或者采集频率要求很高,查询等待会占住 CPU。常见做法是给 AD2S1210 单独一个 SPI 口,或者把读取放到定时器中断里,用状态机分步推进。这个取舍要看具体负载,不是查询就一定差,关键是别在中断里做长延时。

4. AD2S1210 复位、模式切换与读写函数拆解

4.1 复位函数里的延时不是摆设

ad2s1210_Init() 这段代码看着就是拉高拉低几根线,但每个延时都对应数据手册里的时间要求:

void ad2s1210_Init(void) { CLR_RESET(); // 拉低复位 SET_SAMPLE(); // SAMPLE 保持高 delay_us(15); // 复位低电平至少保持 10us SET_RESET(); // 释放复位 delay_ms(60); // 等待内部初始化,与分辨率有关 CLR_SAMPLE(); // 触发一次采样 delay_us(15); SET_SAMPLE(); CLR_SOE(); // SOE 低电平选择串行接口 SET_RD(); // 串行模式下 RD 保持高 CLR_CS(); // 片选拉低,准备通信 }

复位低电平保持时间不够,芯片可能没真正复位,表现是配置寄存器写进去读出来对不上。释放复位后的 60ms 等待和分辨率相关,分辨率越高内部初始化越久,这个值给短了会出现前几次读取全是无效数据。SOE 拉低是选择串行接口的关键,如果这根线悬空或者被拉高,芯片走的是并行接口逻辑,SPI 怎么发都没反应。RD 在串行模式下必须保持高,这点很多人会忽略,因为它名字叫“读”,直觉上以为读的时候要拉低,实际上串行模式下它不参与字节传输。

4.2 模式选择:A0/A1 决定芯片在干什么

AD2S1210 有配置、位置、速度几种工作模式,靠 A0 和 A1 两根线组合切换。AD2S1210_ModeSelect() 就是把传入的枚举值翻译成这两根线的电平:

void AD2S1210_ModeSelect(unsigned char mode) { switch(mode) { case CONFIG: // 配置模式,读写寄存器 CLR_A0(); CLR_A1(); break; case POSITION: // 位置模式,读角度 SET_A0(); CLR_A1(); break; case VELOCITY: // 速度模式,读转速 CLR_A0(); SET_A1(); break; default: break; } }

模式切换后芯片内部需要一点时间稳定,所以读写函数里在切完模式后会有 Delay 或者靠 SAMPLE 的时序来兜底。实际调试时如果发现切到位置模式后第一次读出来是旧值,多半是切换后没等够,加个短延时就能解决。这个延时不用很长,微秒级就够,但省掉它就会变成玄学问题。

4.3 写函数:地址和数据分两次传

写寄存器的协议是先把地址发出去,拉一下 WR,再把数据发出去,再拉一下 WR:

void WriteToAD2S1210(unsigned char address, unsigned char data) { AD2S1210_ModeSelect(CONFIG); // 写寄存器必须在配置模式 CLR_WR(); SPI_Byte(address); // 先发地址 SET_WR(); CLR_WR(); SPI_Byte(data); // 再发数据 SET_WR(); }

WR 的下降沿告诉芯片“接下来这个字节是地址或数据”,上升沿锁存。两次传输之间 WR 要回到高电平,否则芯片分不清这是两个独立的字节。这里没有额外延时,是因为 SPI_Byte 内部的查询等待已经保证了字节传完,WR 的翻转发生在传输完成之后,时序上是安全的。但如果你的 SPI 时钟配得特别快,建议在 SET_WR 之后加一两个空操作,给芯片一点建立时间。

4.4 读函数:配置模式和位置/速度模式走不同路径

读函数是整份代码里分支最多的部分,因为配置模式读寄存器和位置模式读角度,协议完全不一样:

void ReadFromAD2S1210(unsigned char mode, unsigned char address, unsigned char *buf) { AD2S1210_ModeSelect(mode); if(mode == CONFIG) { // 配置模式:发地址,然后读回寄存器值 CLR_WR(); buf[1] = SPI_Byte(address); SET_WR(); Delay(2); CLR_WR(); buf[2] = SPI_Byte(address); SET_WR(); } else if(mode == POSITION || mode == VELOCITY) { // 位置/速度模式:先 SAMPLE 触发,再连续读 3 字节 SET_SAMPLE(); Delay(1); CLR_SAMPLE(); Delay(2); CLR_WR(); buf[2] = SPI_Byte(address); buf[1] = SPI_Byte(address); buf[0] = SPI_Byte(address); SET_WR(); } }

配置模式下发地址是为了选中寄存器,随后读回的是该寄存器的值,代码里 buf[1] 和 buf[2] 的赋值顺序对应两次传输。位置和速度模式下不需要地址,芯片直接把 16 位角度或速度数据以 3 字节形式吐出来,所以连续调三次 SPI_Byte,按 buf[2]、buf[1]、buf[0] 的顺序拼。这个顺序如果搞反,拼出来的角度会变成字节颠倒的乱值,现象是角度跳变但又不是完全随机,很容易误判成机械安装问题。

SAMPLE 的时序也要注意:拉低再拉高触发一次采样,中间的低电平时间和之后的等待时间决定了数据什么时候有效。Delay(2) 给的是数据更新留的窗口,给太短读到的还是上一次的采样值。这套“先触发再读”的流程在位置模式里是必须的,配置模式则不需要 SAMPLE。

5. 寄存器配置与主循环采集:把角度真正读出来

5.1 AD2S1210_Config 里每个值的含义

配置函数把几个关键寄存器一次性写好,不写的保持默认值:

void AD2S1210_Config(void) { WriteToAD2S1210(0x91, 0x08); // 激励频率相关:32768 * 3k / 8.192M WriteToAD2S1210(0x92, 0x7f); // 分辨率设为 16 位 WriteToAD2S1210(0x8c, 0x01); // DOS 复位最小阈值 WriteToAD2S1210(0x88, 0x01); // LOS 阈值 WriteToAD2S1210(0x89, 0x7f); // DOS 超量程阈值 WriteToAD2S1210(0x8a, 0x7f); // DOS 失配阈值 }

0x91 这个寄存器跟激励频率有关,0x08 是按 32768 乘以 3k 再除以 8.192M 算出来的分频值,实际项目里要按你板子上的晶振和旋变激励频率重新算,直接抄这个值可能得到错误的激励频率,导致解码精度下降。0x92 写 0x7f 把分辨率设成 16 位,这是最常用的精度档位,如果你的机械系统只需要 12 位,可以改这里降低对时序的要求。后面几个 0x8c、0x88、0x89、0x8a 是故障检测阈值,LOS 是信号丢失,DOS 是信号劣化,阈值设得太敏感会频繁报故障,设得太松又起不到保护作用,0x01 和 0x7f 是比较保守的起点。

注意:改 0x91 之前先把激励频率的公式在纸上算一遍,写错这个寄存器不会报错,但角度会有周期性误差,很难查。

5.2 主函数里的初始化与采集节奏

主函数的逻辑很直白:系统初始化、SPI 初始化、AD2S1210 复位、写配置、然后进循环读位置:

void main(void) { InitSysCtrl(); // 系统时钟、外设时钟 InitSpiaGpio(); // SPI 引脚复用 SPI_Init(); // SPIA 寄存器配置 AD2S1210_GPIO_Init(); // 控制引脚方向与初始电平 ad2s1210_Init(); // 复位芯片 AD2S1210_Config(); // 写寄存器 unsigned char buf[3]; while(1) { ReadFromAD2S1210(POSITION, 0x00, buf); // buf[0] 高字节,buf[1] 中字节,buf[2] 低字节 unsigned int angle = ((unsigned int)buf[0] << 8) | buf[1]; // 这里把 angle 送进控制环或上报 delay_us(100); } }

循环里读的是位置模式,返回 3 字节,实际角度是 16 位,用前两字节拼出来即可,第三字节在 16 位分辨率下是状态或低位扩展,具体看芯片手册的帧格式。采集周期用 delay_us(100) 控制,对应大约 10kHz 的读取频率,实际项目里这个节奏要跟电流环或速度环匹配,读太快没意义,读太慢控制带宽上不去。如果发现角度在静止时也有小幅抖动,先看是不是采样触发和读取之间的延时不够,再看旋变激励信号的质量。

5.3 数据拼装与量纲换算

读回来的原始值是数字量,要变成弧度或度还需要换算。16 位分辨率下,满量程对应 2π,所以角度 = raw / 65536 * 2π。这个换算放在主循环里做还是放到上层控制代码里做,取决于你的分层习惯。我一般把原始值往上送,换算放在应用层,这样底层驱动保持通用,换分辨率时只改一处。另外要注意 buf 的符号和拼接顺序,前面提过顺序反了会得到字节颠倒的值,调试时可以先打印三个字节的十六进制,手动拼一遍对照,确认无误再进控制环。

6. 二次开发与调试:换分辨率、换 SPI 口、加故障检测怎么改

拿到这份代码之后,真正花时间的往往不是让它跑起来,而是让它在你自己的板子上稳定跑。第一个改造点是分辨率。0x92 写 0x7f 是 16 位,如果你改成 12 位,读取函数里拼字节的逻辑要跟着变,位置模式下返回的有效字节数可能不同,换算系数也要从 65536 改成 4096。改完先用静态角度验证,把旋变固定在一个已知角度,看读回来的值是否落在预期区间,别直接上闭环。

第二个改造点是 SPI 口。这份代码用的是 SPIA,如果你的板子 SPIA 被别的外设占了,要换到 SPIB 或 SPIC,需要改的不只是 SpiaRegs 前缀,还有 InitSpiaGpio 对应的引脚复用、LSPCLK 的分频假设、以及中断向量。我一般会把寄存器访问封装成一层宏或内联函数,换口时只改这一层,上层读写函数不动。下面是一个简单的封装思路:

// spi_port.h:换口时只改这个文件 #define SPI_TXBUF SpiaRegs.SPITXBUF #define SPI_RXBUF SpiaRegs.SPI #define SPI_STS SpiaRegs.SPISTS.bit.INT_FLAG #define SPI_BRR SpiaRegs.SPIBRR

第三个改造点是故障检测。AD2S1210 有 LOS、DOS 这些故障位,代码里配了阈值但没读状态。实际项目里应该在每次读角度时顺带读一下故障寄存器,一旦置位就上报或切到安全状态。做法是在 ReadFromAD2S1210 之后加一次配置模式读故障寄存器,注意模式切换本身有开销,不要每个控制周期都切,可以每 N 个周期查一次。

改造项改动位置验证方法
分辨率 16→12 位0x92 寄存器、拼字节与换算固定角度对比原始值
SPIA→SPIB引脚复用、寄存器宏、LSPCLK示波器看时钟与片选
加故障检测读状态寄存器、模式切换节奏人为断激励信号看是否上报
提高采集频率主循环延时、是否改中断看角度更新是否跟得上

最后说个血泪经验:这套代码里最容易被忽略的不是 SPI 配置,而是那几根控制 GPIO 的初始电平。上电瞬间如果 CS 或 WR 处于错误电平,芯片可能进入未定义状态,后面怎么复位都救不回来。我现在的习惯是在 GPIO 初始化里先把所有控制线设成安全电平(CS 高、WR 高、RESET 低),再走复位流程,从那以后每次新板子 bring-up 都强制走一遍这个顺序。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表