你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

NUCLEO-U545RE-Q板卡评测 (第二弹) U545移植TMC_API

[复制链接]
watershade 发布时间:2023-11-22 14:24

NUCLEO-U545RE-Q板卡评测

(第二弹)U545移植TMC_API


一、前言

这篇文章的编写耗费了一些时间。因为U5是一个完全不同的架构,我尝试了TrustZone的方式编写程序,发现对其中的一些概念还有些模糊。U5很牛的LPBAM也还没有玩转,所以架构部分的分享可能要稍晚一些再做。 这次趁着手边有TMC22xx的开发板,刚好自己也正在设计开发一个STM32G0系列的开发板。那就顺便以U5作为载体,先验证TMC_API的移植。目前移植的过程我分成了两步,先编写基础的BSP部分,以实现LOG打印和步进电机的传统控制(STEP/DIR/EN);然后移植TMC_API。 当然可能有人不了解TMC22XX,我在这里简单介绍一下。TMC2208/TMC2209是常见的静音步进电机驱动,很多3D打印机会使用这种驱动实现比较好的步进控制。它的厂家是Trinamic,先是在20年被Maxim并购,接着巨无霸ADI又把Maxim并购。所以现在Trinamic是ADI的一部分。上半年Trinamic似乎还有自己的网站,现在被ADI整合。整合后很多资料都不好找了。不像以前那样产品分类井井有条,资料也很容易找。

二、U5的X-CUBE-AZURERTOS在哪里?

在刚开始接触U5的时候,我最初其实以为U5还没有移植threadx。但我想U5已经发布一年多了应该不会吧。像其它系列那样我移植在官网、社区寻找 X-CUBE-AZURERTOS-U5可是苦苦寻找不到。在查看x-cube-u5的文档的时候发现架构里面明明是有threadx的。后来查看UM288(Getting started with STM32CubeU5 for STM32U5 Series)时看到了一幅图: STM32CubeU5_MCU_package_components.png

ThreadX赫然放在中间层(Level 1),除了ThreadX之外当然还有FileX/LevelX,USBX,NetX Duo等。这时我就在思考为什么U5没有将X-CUBE-AZURERTOS独立成一部份,是不是因为上层(Level 2)的某些组件是需要依赖于ThreadX的呐?目前还没有答案,有知道的朋友请不吝赐教。

三、GPDMA和LOG的实现

有开发经验的朋友都知道,传统的printf其实非常的低效。每次都是一个字节一个字节输出的。对于有DMA的设备这样用UART其实非常的低效。所以在开发时往往会自己编写一个使用DMA传输的printf。如果你还用RTOS,那么可以结合queue来将需要发送的信息索引放入缓冲栈中,然后再由一个线程在相对空闲时将需要发送的信息通过UART的MDA形式发送出去。 至于需要发送的信息怎么放入,当前使用的时RTOS的mempool的形式。每条信息都需要占用一个固定的长度。这种方式在消息比较少时还可以。在连续多条时很快就会将会内存池占用完,这就会导致信息的漏发。后面准备开发一个RingBuffer来管理不需要分条发送的信息,这种信息(比如dump信息)可以使用RingBuffer开发送。这样可以避免漏发的问题。 这部分代码暂不公开了,可以查看我的github的项目UART_Receive_WithIdle了解类似的代码。这里用的板卡是Nucleo-H723ZG,大同小异。后面等U5的这部分程序完全开发完毕,再公开项目到github.具体信息看本次评测的后续情况。

这里需要重点说明的是U5的DMA开发过程完全不一样,U5有GPDMA1和LPDMA1两组DMA。前者在Main Domain,后者在SDR Domain。外设和SDRAM的选择非常重要,否则可能会导致hardfault或者DMA的效率底下。GPDMA功能非常强大,除了普通模式还支持链表模式。之于GPDMA的使用其实可以单开一篇文章。这篇文章的重点不是将GPDMA,所以就简单说明一下。 感兴趣的朋友可以查看<font color=red>AN5593-如何对STM32U575&585微控制器使用GPDMA</font>.这篇文章对GPDMA有很好的讲解,从通道的配置到不同外设的注意事项都很详尽。这一必须再款将一下ST,我觉得ST的技术支持工作真的很好。非常方便个人开发者自学和深入。

四、OC功能和步进电机的控制

4.1 STEP/DIR/EN控制介绍

步进电机的传统控制方式经常使用STEP/DIR/EN引脚来控制。STEP需要产生一个需速度匹配的脉冲;DIR和EN都是普通的输出信息。DIR的高低电平分别代表步进电机的方向(或者说A+,A-,B+,B-的电平变化顺序),EN,有的叫ENN,用高电平或者低电平取使能步进电机的输出。步进电机不像其它普通的电机,当输出Enable时,电机会被锁死在固定位置。直到step将它移动到下一个位置。现在大部分的步进电机驱动器都支持MicroStep技术(微步技术)。那么什么是微步技术,你可以想象成段誉的凌波微步(不是),或者戏剧舞台上的鬼步。就是将一大步分成无数的小碎步。比如现在常见的电机是1.8°的,就是一圈是200步。当然也有更精密的0.9°步进电机,一圈就是400步。微步就是将一步分成4步、8步、16步、64步甚至128步或者256步。拿TMC2209为例: TMC2209的微步配置.png

可以看到它有8,16,32,64微步技术。比如如果我将MS1和MS2是内部下拉的,如果将它们悬空,那么电机要走一大步就是要走8小步,step必须给8*200=1600个脉冲,电机才能转一圈。微步如果越是大,电机就越是平滑。当然带来的坏处是,电机的“冲力”就小。可以想象一下你大跨步冲上陡坡和用小碎步冲上陡坡。不过这通常影响不大一般我们还是选择大一点的微步,这样电机带来的抖动会大大降低。再结合RAMP技术,就会带来非凡的体验。 步进电机的控制其实也可以用FOC,不过这里就暂时不去深究这一部分。有兴趣的朋友可以在这一部分多加了解。

4.2 STEP信号的产生

其实步进电机传统控制最关键的是STEP信号的产生。对于脉冲的产生,我们通常会想到的方式是PWM方式。当然也可以用定时器去定时反转IO引脚。但是前者如果要改变速度你就需要改变两个量,一个是Period的计数值一个是Duty的计数值。后者需要CPU参与去反转。其实有一种更好的配置是OC(Output Compare)方式。输出比较切换模式提供了在保持占空比固定的情况下的,只改变Period的可能。这样可以尽可能少的占用CPU的负载。 至于OC的使用,有兴趣的朋友可以看ST社区发布的一篇文章LAT1155 使用输出比较模式产生相移信号.其实我记得还有一篇介绍,但是苦于找不到。 这里说他一个小插曲。正如LAT1155里面的示例所显示的那样CCRx寄存器实际是用来改变相位的。要改变period其实要改变的是CNT值(ARR寄存器)。但是我最初改变的是CCRx的值,这就导致了无论怎么搞周期始终不变。 其实这一步还有一个很trick的地方是怎么开启DMA。因为我们要改变的是周期,所以正如上文所说其实需要改变的是CNR或者AAR寄存器。如果你使用HAL_TIM_OC_Start_DMA去开启DMA传输,但是GPDMA其实链接的是TIM_UP,会进入hardfault。而你不能错误的将DMA通道链接到TIM_CCRx上。因为这其实改变的是触发的相位而非周期。这就需要你自己编写一个开启OC同时开启DMA的方法。 具体怎么做其实也不复杂:

HAL_StatusTypeDef state = HAL_OK;
  // 省略部分代码
  state = HAL_TIM_Base_Start_DMA(&TMC_STEP_TIM_HANDLE, tmc_dma_cfg.src, tmc_dma_cfg.len);
  if (state == HAL_OK)
  {
    state = HAL_TIM_OC_Start(&TMC_STEP_TIM_HANDLE, TMC_STEP_TIM_CHANNEL);
    if (state != HAL_OK)
    {
      HAL_TIM_Base_Stop_DMA(&TMC_STEP_TIM_HANDLE);
    }
  }

在描述的最后献上测试视频: 第一个视频是:多个速度和正反转控制

TMC_API移植准备之一:步进电机测试_哔哩哔哩_bilibili

<iframe src="https://player.bilibili.com/player.html?aid=963670709&bvid=BV1AH4y127qv&cid=1340402023&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

第二个视频是:加减速测试 <video src="https://www.bilibili.com/video/BV1ou4y1L7hc/?spm_id_from=333.999.0.0">你的网站不支持video标签</video>

<iframe src="https://player.bilibili.com/player.html?aid=918631946&bvid=BV1ou4y1L7hc&cid=1339959530&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

4.3 INDEX信号

TMC有个index信号用来记录波形的数量,这个信号如果引出可以用一个TIMER的计数模式去记录,这样就可以"伪闭环"的方式记录已经发出的脉冲数量,这样可以进一步去停止或者改变下一步的波形。 但如果没有index信号,也可以将将OC的trig信号输出,然后通过计数模式去记录发出去的波形数量。同时可以用DIR信号去控制计数是递增还是递减,这样也从某种意义上实现了一个“软编码器”。当然这个“软编码器”实际是个假的编码器。

4.4 RAMP

步进电机的加减速过程其实非常重要。我们常用的时Linear加减速。但是实际上如果考虑时间因素的话,很多加减速难以计算。具体可以查看这篇文章 对于更复杂的S-RAMP它的计算就更复杂了,这一部分我也在仔细研究。暂时没有什么好分享的。

五、TMC_API的移植

终于来到我们最关键的TMC_API的移植。在移植之前我们肯定需要了解的是为什么移植。

5.1 TMC_API是什么,我为什么要移植TMC_API

这里是TMC_API的简单介绍:

The TMC-API is a portable C library for working with Trinamic ICs in embedded projects. It is CPU-independent and self-contained (no external dependencies).
The TMC-API allows easy access to the registers of Trinamic ICs. It provides definitions for all IC registers and their bitfields.

我自己之所以选择要移植TMC_API除了好奇之外,还是想在使用TIMER实现繁琐的RAMP实现之外,尝试使用UART/SPI等总线自由的配置它的参数。自己在根据手册,去hardcore每种芯片的驱动显得吃力不讨好。 目前预想的结果是移植之后,TMC的多数芯片我都可以实现通过总线控制。这样我甚至可以省下一些硬件成本(比如不用电位器去手动调节电流大小,比如微步的设置也可以全部从软件实现就节省了拨码开关的成本。) 更加理想的是可以下一步对接TMCL_IDE,通过电脑界面简单配置驱动器。

5.2 TMC_API移植分析

我们以TMC2209.h和TMC2240.h 下面是TMC2209.h的几个核心函数:

// Communication
void tmc2209_writeInt(TMC2209TypeDef *tmc2209, uint8_t address, int32_t value);
int32_t tmc2209_readInt(TMC2209TypeDef *tmc2209, uint8_t address);

void tmc2209_init(TMC2209TypeDef *tmc2209, uint8_t channel, uint8_t slaveAddress, ConfigurationTypeDef *tmc2209_config, const int32_t *registerResetState);
uint8_t tmc2209_reset(TMC2209TypeDef *tmc2209);
uint8_t tmc2209_restore(TMC2209TypeDef *tmc2209);
void tmc2209_setRegisterResetState(TMC2209TypeDef *tmc2209, const int32_t *resetState);
void tmc2209_setCallback(TMC2209TypeDef *tmc2209, tmc2209_callback callback);
void tmc2209_periodicJob(TMC2209TypeDef *tmc2209, uint32_t tick);

uint8_t tmc2209_get_slave(TMC2209TypeDef *tmc2209);
void tmc2209_set_slave(TMC2209TypeDef *tmc2209, uint8_t slaveAddress);

下面是TMC2240.h的几个核心函数

void tmc2240_init(TMC2240TypeDef *tmc2240, uint8_t channel, ConfigurationTypeDef *config, const int32_t *registerResetState);
//void tmc2240_fillShadowRegisters(TMC2240TypeDef *tmc2240);
uint8_t tmc2240_reset(TMC2240TypeDef *tmc2240);
uint8_t tmc2240_restore(TMC2240TypeDef *tmc2240);
uint8_t tmc2240_getSlaveAddress(TMC2240TypeDef *tmc2240);
void tmc2240_setSlaveAddress(TMC2240TypeDef *tmc2240, uint8_t slaveAddress);
void tmc2240_setRegisterResetState(TMC2240TypeDef *tmc2240, const int32_t *resetState);
void tmc2240_setCallback(TMC2240TypeDef *tmc2240, tmc2240_callback callback);
void tmc2240_periodicJob(TMC2240TypeDef *tmc2240, uint32_t tick);

uint8_t tmc2240_consistencyCheck(TMC2240TypeDef *tmc2240);

分析后发现上面的几个函数都是应用函数,那么需要移植的函数在哪里? 我们查看TMC2209.c会发现

// => UART wrapper
extern void tmc2209_readWriteArray(uint8_t channel, uint8_t *data, size_t writeLength, size_t readLength);
// <= UART wrapper

// => CRC wrapper
extern uint8_t tmc2209_CRC8(uint8_t *data, size_t length);
// <= CRC wrapper

同样的TMC2130.c中有:

// => SPI wrapper
// Send [length] bytes stored in the [data] array over SPI and overwrite [data]
// with the reply. The first byte sent/received is data[0].
extern void tmc2130_readWriteArray(uint8_t channel, uint8_t *data, size_t length);
// <= SPI wrapper

但是在TMC2240.c中,略有不同:

extern void tmc2240_writeInt(TMC2240TypeDef *tmc2240, uint8_t address, int32_t value);

通过上面的分析我们大概能否明白怎么移植,至于更细节的部分可能要结合具体实现来了解。 其实TMC_API工程的ReadMe也有简单的介绍,只是不够准确:

To use the TMC-API, perform the following steps in your code:

1. Implement the tmcXXXX_readWriteArray() function in in your code. This function provides the necessary hardware access to the TMC-API.
在自己的代码中实现tmcXXXX_readWriteArray()。此代码用来提供TMC-API必要的硬件读写功能。

2. Call tmcXXXX_init() once for each Trinamic IC in your design. This function initializes an IC object which represents one physical IC.
初始化的时候需要调用tmcXXXX_init函数实现对每一款芯片的初始化。

3. Call tmcXXXX_periodicJob() periodically. Pass a millisecond timestamp as the tick parameter.
需要设法在自己的代码中周期性的执行tmcXXXX_periodicJob()函数。需要传入当前的毫秒时间戳给它。

4. After initializing, calling tmcXXXX_reset() or tmcXXXX_restore(), the TMC-API will write multiple registers to the IC (referred to as IC configuration). Per call to tmcXXXX_periodicJob(), one register will be written until IC configuration is completed.
初始化完成之后,可以调用tmcXXXX_reset()去将参数恢复到出厂设置,使用tmcXXXX_restore()实现回退临时的修改。两个具体区别,看一查看writeConfiguration函数了解详情。

5. Once the IC configuration is completed, you can use tmcXXXX_readInt() and tmcXXXX_writeInt() to read and write registers.
一旦芯片的配置完成,用户可以使用tmcXXXX_readInt和tmcXXXX_writeInt的读写寄存器。

上面的中文和标签是我自行修改的。方便大家理解。

另外需要说明的是移植工程时只需要将工程的<span style="color:red; font-weight:bold">tmc目录所在的路径</span>包含进去即可。因为所有的TMC_API的header都是以tmc/...include进去的。

5.3 深入分析TMC_API的功能

TMC_API的tmc下包含了三个目录:helpers, ic和ramp.

  1. helpers目录下面API_Header.h索引了Bits.h,Config.h,Constant.h,CRC.h,Macros.h,RegisterAccess.h,Types.h等。除了索引的这几个头文件之外。还有一个Functions.h,此文件被LinearRamp1调用。查看Functions.c中函数的定义可以知道其实这里定义了几个数学函数,通过查表法,帮助快速执行结果。CRC.c定义了一些CRC如软计算相关的操作。使用这个CRC会占用较大的空间。因为STM32的很多芯片都有CRC,直接使用硬件CRC可能会更好一些。就先不细致研究。
  2. ic目录下包含了各个支持的芯片(TMC21XX/TMC22XX系列,TMC4XXX/TMC5XXX系列和一款MAX22216)驱动。而每个芯片的驱动基本上(少数不同)包含4个头文件(#IC_Constants.h,#IC_Fields.h,#IC_Registers.h和#IC.h)和1个源文件(#IC.c)。[上面的#IC表示芯片名称。]
  3. ramp目录包含了几种加速曲线的实现。具体不再赘述

5.4 移植过程

编写移植的函数。按照上面的分析主要时读写函数的移植、CRC的移植(如果有必要),然后编写初始化函数,周期性调用tmcXXXX_periodicJob。 这里涉及的具体工作很多,具体不再赘述。 [坑比较深,正在移植...]

5.5 移植TMCL

TMCL如果实现可以使用TMCL-IDE来控制。 [正在移植...]

5.6 实现移植的效果

[敬请等待...]

收藏 评论3 发布时间:2023-11-22 14:24

举报

3个回答
h12121 回答时间:2023-12-11 18:08:08

移植效果有了么哈哈

STMWoodData 回答时间:2023-12-12 11:01:30

期待一下移植效果,这个还没玩过呢。学习学习

watershade 回答时间:2023-12-12 11:45:13

h12121 发表于 2023-12-11 18:08
移植效果有了么哈哈

已经尝试移植了,TMCL尝试了握手部分,已经可以被TMCL-IDE识别了。但是手边的项目忙,预计要忙完之后再接着移植。

感谢关注

关于意法半导体
我们是谁
投资者关系
意法半导体可持续发展举措
创新和工艺
招聘信息
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
关注我们
st-img 微信公众号
st-img 手机版