前言最近做的一个小项目需要使用到RT-Thread，借此机会了解了一下RT-Thread和它提供的BSP框架，并简单分析移植方法。关于RT-Thread和它的BSP，官方已经提供了比较详尽的文档，并且目前已经有部分厂家（比如最常见的STM32）的BSP模版可供使用：RT-Thread标准版本文档STM32系列BSP制作教程BSP工程目录总览本文以笔者手上的weact的stm32h750开发板BSP为例进行分析Applications:应用层代码Compiler:ARM的C语言库DeviceDrivers:RT-Thread的Device框架驱动程序Drivers:包括Device框架中各种设备在STM32外设上的实现以及开发板上外设的驱动程序FAL,Filesystem,Finsh:都是一些RT-Thread的可选组件Kernel:RT-Thread内核文件libcpu:RT-Thread内核移植需要进行实现的API和一些与单片机内核相关的函数Libraries:HAL库如何让内核在你的芯片上跑起来内核的移植首先是拷贝操作系统内核源码，然后对中断使能，上下文切换等依赖于具体单片机内核的功能进行实现操作系统源码这部分代码放在rt-thread仓库的src目录下，对应的头文件在include目录下。注意到这堆文件都加了个编译预处理选项：找了一下发现这个宏定义出现在了include/rtsched.h里：/** * NOTE: user should NEVER use these APIs directly. See rt_thread_.* or IPC * methods instead. */ #if defined(__RT_KERNEL_SOURCE__) || defined(__RT_IPC_SOURCE__) /* thread initialization and startup routine */ void rt_sched_thread_init_ctx(struct rt_thread *thread, rt_uint32_t tick, rt_uint8_t priority); void rt_sched_thread_init_priv(struct rt_thread *thread, rt_uint32_t tick, rt_uint8_t priority); void rt_sched_thread_startup(struct rt_thread *thread); /* scheduler related routine */ void rt_sched_post_ctx_switch(struct rt_thread *thread); rt_err_t rt_sched_tick_increase(void); /* thread status operation */ rt_uint8_t rt_sched_thread_get_stat(struct rt_thread *thread); rt_uint8_t rt_sched_thread_get_curr_prio(struct rt_thread *thread); rt_uint8_t rt_sched_thread_get_init_prio(struct rt_thread *thread); rt_err_t rt_sched_thread_yield(struct rt_thread *thread); rt_err_t rt_sched_thread_close(struct rt_thread *thread); rt_err_t rt_sched_thread_ready(struct rt_thread *thread); rt_err_t rt_sched_thread_suspend(struct rt_thread *thread, rt_sched_lock_level_t level); rt_err_t rt_sched_thread_change_priority(struct rt_thread *thread, rt_uint8_t priority); rt_err_t rt_sched_thread_bind_cpu(struct rt_thread *thread, int cpu); rt_uint8_t rt_sched_thread_is_suspended(struct rt_thread *thread); rt_err_t rt_sched_thread_timer_stop(struct rt_thread *thread); rt_err_t rt_sched_thread_timer_start(struct rt_thread *thread); void rt_sched_insert_thread(struct rt_thread *thread); void rt_sched_remove_thread(struct rt_thread *thread); #endif /* defined(__RT_KERNEL_SOURCE__) || defined(__RT_IPC_SOURCE__) */这个文件主要是一些线程管理相关的API，可以看出来这个宏定义是为了确保这些API只能由内核调用，防止用户在奇怪的地方调用。如果内核文件没加上这个宏定义会报符号未定义错误（笔者自己踩的坑），如果自己手动移植要注意这一点。内核API实现虽然RT-Thread的内核教程说要自己实现开关中断之类的函数，但是它已经针对一些常见单片机内核进行了实现，比如说h750的cortex-m7内核。这些API实现在rt-thread仓库的libcpu目录下，主要在context_rvds.S和cpuport.c这两个文件里。经过这两步RT-Thread内核已经可以成功编译并且在你的芯片上运行起来了，如果你只需要使用到这些功能的话其实可以选择RT-Thread的阉割nano版本，nano版本提供了完整的内核功能和一个不需要Device框架（后面会提到）版本的Finsh组件（就是一个命令行工具）Device框架嵌入式系统经常需要用到各种片上或者片外的设备，RT-Thread把硬件上的外设进行了抽象，由此诞生出了Device框架，也许是为了方便应用在不同硬件平台之间的移植？RT-Thread中的内核对象和设备等都采用了类似c++的类继承思想，常用的PIN设备，ADC设备，UART设备等都是在rt_device这个基类上派生出来的，当然你也可以创建自己的设备。每种设备需要实现的API在官方文档中都有详细说明，以最简单的PIN设备为例，下面是在drv_gpio.c中实现的一个PIN设备硬件初始化函数：int rt_hw_pin_init(void) { #if defined(__HAL_RCC_GPIOA_CLK_ENABLE) __HAL_RCC_GPIOA_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOB_CLK_ENABLE) __HAL_RCC_GPIOB_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOC_CLK_ENABLE) __HAL_RCC_GPIOC_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOD_CLK_ENABLE) __HAL_RCC_GPIOD_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOE_CLK_ENABLE) __HAL_RCC_GPIOE_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOF_CLK_ENABLE) __HAL_RCC_GPIOF_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOG_CLK_ENABLE) #ifdef SOC_SERIES_STM32L4 HAL_PWREx_EnableVddIO2(); #endif __HAL_RCC_GPIOG_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOH_CLK_ENABLE) __HAL_RCC_GPIOH_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOI_CLK_ENABLE) __HAL_RCC_GPIOI_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOJ_CLK_ENABLE) __HAL_RCC_GPIOJ_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOK_CLK_ENABLE) __HAL_RCC_GPIOK_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOM_CLK_ENABLE) __HAL_RCC_GPIOM_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPION_CLK_ENABLE) __HAL_RCC_GPION_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOO_CLK_ENABLE) __HAL_RCC_GPIOO_CLK_ENABLE(); #endif #if defined(__HAL_RCC_GPIOP_CLK_ENABLE) __HAL_RCC_GPIOP_CLK_ENABLE(); #endif return rt_device_pin_register("pin", &_stm32_pin_ops, RT_NULL); }该函数在rt_hw_board_init()调用（RT-Thread初始化过程可以参见官方文档），可以看到它的硬件初始化就是把用到的I/O口时钟打开，然后注册一个PIN设备， rt_device_pin_register中的第二个参数就是一堆API的函数指针：static const struct rt_pin_ops _stm32_pin_ops = { stm32_pin_mode, stm32_pin_write, stm32_pin_read, stm32_pin_attach_irq, stm32_pin_dettach_irq, stm32_pin_irq_enable, stm32_pin_get, };其他类型设备的初始化也大体遵循这个顺序，先把硬件初始化好，然后把相关的API注册上去CUBEMX配置经常用STM32的朋友都知道，我们可以用CUBEMX配置各种初始化配置。RT-Thread的Device框架写了很多现成的HAL库外设初始化函数，在rt-thread/bsp/stm32/libraries目录下，因此不需要用CUBEMX进行外设的配置，但是时钟树和外设的引脚配置还是需要用到CUBEMX，可以看到BSP目录底下开了一个CUBEMX工程：这个工程里面其实就只有它生成的那个msp文件有用注意这个选项不要勾，这样所有的初始化函数都会生成到一个c文件里，在外面的工程中包含这个c文件即可至于时钟树的初始化函数，CUBEMX会把它生成到main.c里面，需要把它复制下来粘到工程里的board.c文件里KConfig文件RT-Thread提供了一个ENV工具方便对不需要的设备或组件进行裁剪，其实就是自动生成一个rtconfig.h文件，里面有一堆宏定义，用户在初始化函数中用#ifdef进行条件编译实现裁剪，下面是该BSP的KConfig文件：menu "Hardware Drivers Config" config SOC_STM32H750VBT6 bool select SOC_SERIES_STM32H7 select RT_USING_COMPONENTS_INIT select RT_USING_USER_MAIN default y menu "Onboard Peripheral Drivers" config BSP_USING_SPI_FLASH bool "Enable SPI FLASH (W25Q64 spi1)" select BSP_USING_SPI select BSP_USING_SPI1 select RT_USING_DFS select PKG_USING_LITTLEFS select RT_USING_MTD_NOR select RT_USING_FAL select FAL_USING_SFUD_PORT select RT_USING_SFUD select RT_SFUD_USING_SFDP default n config BSP_USING_QSPI_FLASH bool "Enable QSPI FLASH (W25Q64)" select BSP_USING_QSPI select FAL_USING_SFUD_PORT select RT_USING_SFUD select RT_SFUD_USING_QSPI default n config BSP_USING_LCD_SPI bool "Enable 0.96' TFT-LCD(ST7735S)" select BSP_USING_GPIO select BSP_USING_SPI select BSP_USING_SPI4 # select BSP_SPI4_TX_USING_DMA select BSP_USING_PWM select BSP_USING_PWM1 select BSP_USING_PWM1_CH2 default n if BSP_USING_LCD_SPI choice prompt "choice back light" default LCD_BACKLIGHT_USING_GPIO config LCD_BACKLIGHT_USING_PWM bool "LCD_BACKLIGHT_USING_PWM(tim1_ch2 pwm1)" config LCD_BACKLIGHT_USING_GPIO bool "LCD_BACKLIGHT_USING_GPIO(PE10)" endchoice endif endmenu menu "On-chip Peripheral Drivers" config BSP_USING_GPIO bool "Enable GPIO" select RT_USING_PIN default y menuconfig BSP_USING_UART bool "Enable UART" default y select RT_USING_SERIAL if BSP_USING_UART config BSP_USING_UART1 bool "Enable UART1" default y config BSP_UART1_RX_USING_DMA bool "Enable UART1 RX DMA" depends on BSP_USING_UART1 && RT_SERIAL_USING_DMA default n endif config BSP_USING_QSPI bool "Enable QSPI BUS" select RT_USING_QSPI select RT_USING_SPI default n menuconfig BSP_USING_SPI bool "Enable SPI BUS" default n select RT_USING_SPI if BSP_USING_SPI config BSP_USING_SPI1 bool "Enable SPI1 BUS" default n config BSP_USING_SPI4 bool "Enable SPI4 BUS" default n endif menuconfig BSP_USING_I2C1 bool "Enable I2C1 BUS (software simulation)" default n select RT_USING_I2C select RT_USING_I2C_BITOPS select RT_USING_PIN if BSP_USING_I2C1 config BSP_I2C1_SCL_PIN int "i2c1 scl pin number" range 1 216 default 135 config BSP_I2C1_SDA_PIN int "I2C1 sda pin number" range 1 216 default 95 endif menuconfig BSP_USING_PWM bool "Enable pwm" default n select RT_USING_PWM if BSP_USING_PWM menuconfig BSP_USING_PWM1 bool "Enable timer1 output pwm" default n if BSP_USING_PWM1 config BSP_USING_PWM1_CH1 bool "Enable PWM1 channel1" default n config BSP_USING_PWM1_CH2 bool "Enable PWM1 channel2" default n config BSP_USING_PWM1_CH3 bool "Enable PWM1 channel3" default n config BSP_USING_PWM1_CH4 bool "Enable PWM1 channel4" default n endif endif menuconfig BSP_USING_ADC bool "Enable ADC" default n select RT_USING_ADC if BSP_USING_ADC config BSP_USING_ADC1 bool "Enable ADC1" default n config BSP_USING_ADC2 bool "Enable ADC2" default n config BSP_USING_ADC3 bool "Enable ADC3" default n endif config BSP_USING_ON_CHIP_FLASH bool "Enable on-chip FLASH" default n config BSP_USING_SDIO bool "Enable SDIO" select RT_USING_SDIO select RT_USING_DFS default n config BSP_USING_USBD bool "Enable OTGHS as USB device" select RT_USING_USB_DEVICE select BSP_USBD_TYPE_HS select BSP_USBD_SPEED_HS select BSP_USBD_PHY_ULPI default n source "$BSP_DIR/../libraries/HAL_Drivers/drivers/Kconfig" endmenu menu "Board extended module Drivers" endmenu endmenu关于详细的语法描述可以参考KConfig语法及示例，配置好该文件后在BSP目录下打开ENV工具就可以看到一个这样的菜单：在菜单里面勾选或者取消勾选，对应的宏定义就会在生成的rtconfig.h头文件里面出现或者消失，也可以进行一些参数的配置，最后在工程中包含该头文件即可
简介在电力电子应用中单片机经常需要和各种外围设备进行通信，本文主要介绍几种常用的通信接口的使用方法低速时钟(LSPCLK)c2000的一些通信接口（McBSP、SCI、SPI）是将片上的低速时钟信号作为Bitclock的，低速时钟由系统时钟分频得到，默认是四分频，如果需要修改分频系数可以使用下面这个函数static inline void SysCtl_setLowSpeedClock(SysCtl_LSPCLKPrescaler prescaler)串行通信接口(SCI)SCI的发送和接收都有一个16字节的FIFO，还有一些常见的中断信号之类的，和其他串口区别不大捏妈，API手册SCI这块直接消失了，谢谢你，TI初始化设置GPIO//发送引脚 GPIO_setPadConfig(8,GPIO_PIN_TYPE_STD); GPIO_setPinConfig(GPIO_8_SCITXDA); GPIO_setQualificationMode(8,GPIO_QUAL_ASYNC); //接收引脚 GPIO_setPadConfig(9,GPIO_PIN_TYPE_STD); GPIO_setPinConfig(GPIO_9_SCIRXDA); GPIO_setQualificationMode(9,GPIO_QUAL_ASYNC);TI论坛上面有老哥说这个qualification不开也能用，笔者试了一下发现确实能用，按照TI的说法，这个qualification是用来防止在复杂环境下工作时受到各种噪声影响的，手册上面也让把这个打开，所以还是开了（重置SCI SCI_performSoftwareReset(SCIA_BASE);设置通信速率和帧格式波特率通过设置分频寄存器进行配置，手册给出了波特率的计算公式：BRR = (SCIHBAUD << 8) + (SCILBAUD)The SCI baud rate is calculated using the following equation:SCI Asynchronous Baud = LSPCLK / ((BRR + 1) *8)最大的可设置波特率是低速时钟频率除以十六使用Driverlib函数可以自动计算分频系数帧格式需要设置数据位数、停止位数和校验方式 SCI_setConfig(SCIA_BASE,50000000,115200,//低速时钟50MHz,波特率115200 (SCI_CONFIG_WLEN_8| //8位数据位 SCI_CONFIG_STOP_ONE| //1位停止位 SCI_CONFIG_PAR_NONE)); //无校验 设置FIFOSCI_enableFIFO(SCIA_BASE);//使能FIFO SCI_resetTxFIFO(SCIA_BASE);//重置FIFO SCI_resetRxFIFO(SCIA_BASE);设置中断下面这个是SCI的中断信号框图初始化中断向量表（如果没有初始化的话）Interrupt_enablePIE(); Interrupt_initModule(); Interrupt_initVectorTable();关中断DINT;注册中断服务函数，函数使用的中断号可以在hw_ints.h里面找到Interrupt_register(INT_SCIA_RX,Recv_Callback);//注册SCIA的串口接收中断服务函数使能中断Interrupt_enable(INT_SCIA_RX);//在CPU处使能SCIA串口接收中断 SCI_enableInterrupt(SCIA_BASE,SCI_INT_RXFF);//使能SCIA模块的接收中断设置FIFO中断条件，接收和发送可以分别设置SCI_setFIFOInterruptLevel(SCIA_BASE,SCI_FIFO_TX1,SCI_FIFO_RX1); //发送和接收FIFO都在接收到一个字节之后产生中断开中断EINT;其他通信外设的中断玩法和这个差不多，后面就不提了使能SCISCI_enableModule(SCIA_BASE);使用接收中断实现一个简单的echo功能__interrupt void Recv_Callback(void) { uint16_t recv_buffer[1]={0}; SCI_readCharArray(SCIA_BASE,recv_buffer,1); SCI_writeCharArray(SCIA_BASE,recv_buffer,1); SCI_clearOverflowStatus(SCIA_BASE); SCI_clearInterruptStatus(SCIA_BASE, SCI_INT_RXFF); Interrupt_clearACKGroup(INTERRUPT_ACK_GROUP9); }集成电路总线(I2C)初始化设置GPIO//SDA引脚 GPIO_setPadConfig(0,GPIO_PIN_TYPE_PULLUP); GPIO_setPinConfig(GPIO_0_SDAA); GPIO_setQualificationMode(0,GPIO_QUAL_ASYNC); //SCL引脚 GPIO_setPadConfig(1,GPIO_PIN_TYPE_PULLUP); GPIO_setPinConfig(GPIO_1_SCLA); GPIO_setQualificationMode(1,GPIO_QUAL_ASYNC);设置I2C使用下面的函数进行通信速率和时钟信号占空比的设置I2C_initController(I2CA_BASE, 200000000, 100000, I2C_DUTYCYCLE_50); //系统时钟200MHz，通信速率100000Hz，时钟信号占空比50%设置FIFOI2C_enableFIFO(I2CA_BASE);FIFO中断的设置方式和SCI是一样的使能I2CI2C_enableModule(I2CA_BASE);使用I2C进行通信（主模式）帧格式设置I2C_setSlaveAddress(I2CA_BASE, 0x56);//设置从机地址 I2C_setBitCount(I2CA_BASE, I2C_BITCOUNT_8);//设置每帧的数据位数 I2C_setDataCount(I2CA_BASE, 1);//设置要发送或接收的帧数开始条件和停止条件设置I2C_sendStartCondition(I2CA_BASE);//置位开始条件位 I2C_sendStopCondition(I2CA_BASE);//置位停止条件位发送开始条件后I2C会在总线上产生一个开始条件并且开始发送数据，之后将开始条件位清零设置停止条件后(1)如果工作在非连续模式下，I2C每发送一帧数据计数会减小一，当数据计数为零时向总线上发送一个停止条件并且将停止条件位清零(2)如果工作在连续模式下，数据计数不起作用，当停止条件被置位后立即产生一个停止条件注：当I2C工作在非连续模式下并且发送地址没有得到回应时，停止条件位会被直接清零，但不会在总线上产生停止条件，这会导致总线处于繁忙状态，遇到这种情况时需要再一次置位停止条件位以释放总线（手册没有提到这点，相当坑人）工作模式设置I2C_setConfig(I2CA_BASE, I2C_CONTROLLER_SEND_MODE);//主机发送模式 I2C_setConfig(I2CA_BASE, I2C_CONTROLLER_RECEIVE_MODE);//主机接收模式发送和接收void I2C_putData(uint32_t base, uint16_t data)//向发送寄存器送入一帧数据 uint16_t I2C_getData(uint32_t base)//从接收寄存器读取一帧数据串行外设接口(SPI)初始化设置GPIOGPIO_setPadConfig(58,GPIO_PIN_TYPE_PULLUP);//引脚输入时进行内部上拉 GPIO_setPadConfig(59,GPIO_PIN_TYPE_PULLUP); GPIO_setPadConfig(60,GPIO_PIN_TYPE_PULLUP); GPIO_setPadConfig(61,GPIO_PIN_TYPE_PULLUP); GPIO_setPinConfig(GPIO_58_SPISIMOA);//绑定外设 GPIO_setPinConfig(GPIO_59_SPISOMIA); GPIO_setPinConfig(GPIO_60_SPICLKA); GPIO_setPinConfig(GPIO_61_SPISTEA); GPIO_setQualificationMode(58,GPIO_QUAL_ASYNC);//开qualification GPIO_setQualificationMode(59,GPIO_QUAL_ASYNC); GPIO_setQualificationMode(60,GPIO_QUAL_ASYNC); GPIO_setQualificationMode(61,GPIO_QUAL_ASYNC);设置SPI使用下面的函数对协议格式，工作模式等进行设置SPI_setConfig(SPIA_BASE, 50000000, SPI_PROT_POL0PHA0, SPI_MODE_CONTROLLER, 100000, 8); //低速时钟50MHz，使用0型协议,主模式，通信速率100000Hz，一帧8位设置FIFOSPI_enableFIFO(SPIA_BASE);//使能FIFO SPI_resetTxFIFO(SPIA_BASE);//重置FIFO SPI_resetRxFIFO(SPIA_BASE);使能SPISPI_enableModule(SPIA_BASE);使用SPI进行通信使能/禁用发送SPI_enableTalk(SPIA_BASE); SPI_disableTalk(SPIA_BASE);发送SPI_transmitByte(base, txData)//发送一个字节 SPI_transmit16Bits(base, txData)//发送一帧16bit数据 SPI_transmitNBytes(base, txBuffer, numOfWords, txDelay)//发送N个字节 void SPI_writeDataBlockingNonFIFO(uint32_t base, uint16_t data)//等待发送缓冲非满并送入一帧 void SPI_writeDataBlockingFIFO(uint32_t base, uint16_t data)//等待发送FIFO非满并送入一帧 注意：使用最下面两个函数的时候要事先对发送数据进行左对齐，在一帧有N位的情况下，data的低16-N位会被丢弃接收SPI_receiveByte(base, dummyData)//接收一个字节 SPI_receiveNBytes(base, rxBuffer, numOfWords, txDelay)//接收N个字节 uint16_t SPI_readDataBlockingNonFIFO(uint32_t base)//等待接收缓冲非空并接收一帧 uint16_t SPI_readDataBlockingFIFO(uint32_t base)//等待接收FIFO非空并接收一帧 参考链接F28379D Driverlib API手册F28379数据手册F28379技术参考手册
介绍The enhanced pulse width modulator (ePWM) peripheral is a key element in controlling many of the powerelectronic systems found in both commercial and industrial equipment. These systems include digitalmotor control, switch mode power supply control, uninterruptible power supplies (UPS), and other forms ofpower conversion.增强型脉宽调制器(ePWM)外设是控制许多商业和工业设备中电力电子系统的关键元素，这些系统一般包括数字电机控制，开关电源控制，不间断电源控制和其他形式的功率转换系统由上图可见，c2000的epwm被分为了几个子模块，包括时基模块，比较模块，波形发生器模块等GPIO配置首先要配置epwm模块工作时需要使用到的引脚，把输出引脚配成推挽就行void GPIO_setPadConfig(uint32_t pin, uint32_t pinType)pinType的参数：GPIO_PIN_TYPE_STD specifies a push-pull output or a floating inputGPIO_PIN_TYPE_PULLUP specifies the pull-up is enabled for an inputGPIO_PIN_TYPE_OD specifies an open-drain output pinGPIO_PIN_TYPE_INVERT specifies inverted polarity on an input把引脚分配给EPWMvoid GPIO_setPinConfig(uint32_t pinConfig)pinConfig的宏定义可以在pin_map.h里找到,不同的芯片的driverlib可能会有一点区别，比如28379是GPIO_0_EPWM1A,280049就变成了GPIO_0_EPWM1_A我也不知道为什么非要整个不一样的EPWM点灯流程首先禁用计数器的时钟信号SysCtl_disablePeripheral(SYSCTL_PERIPH_CLK_TBCLKSYNC);配置时基(Time-Base)设置分频系数（笔者这里设置了不分频，可以根据需求改）EPWM_setClockPrescaler(EPWM1_BASE,EPWM_CLOCK_DIVIDER_1, EPWM_HSCLOCK_DIVIDER_1);This function sets the pre scaler(divider)value for the time base clock counter and the high speed time base clock counter. Valid values for pre-scaler and highSpeedPrescaler are EPWM_CLOCK_DIVIDER_X, where X is 1,2,4,8,16, 32,64 or 128. The actual numerical values for these macros represent values 0,1…7. The equation for the output clock is: TBCLK = EPWMCLK/(highSpeedPrescaler * pre-scaler)设置计数方向EPWM_setTimeBaseCounterMode(EPWM1_BASE,EPWM_COUNTER_MODE_UP);设置重装载值装载方式EPWM_setPeriodLoadMode(EPWM1_BASE,EPWM_PERIOD_SHADOW_LOAD);EPWM_PERIOD_SHADOW_LOAD 使用影子寄存器EPWM_PERIOD_DIRECT_LOAD 直接访问设置计数器重装载事件EPWM_selectPeriodLoadEvent(EPWM1_BASE, EPWM_SHADOW_LOAD_MODE_COUNTER_ZERO);设置重装载值EPWM_setTimeBasePeriod(EPWM1_BASE,10000);配置比较器(Counter Compare)设置比较值的装载事件EPWM_setCounterCompareShadowLoadMode(EPWM1_BASE, EPWM_COUNTER_COMPARE_A, EPWM_COMP_LOAD_ON_CNTR_ZERO_PERIOD);设置比较值EPWM_setCounterCompareValue(EPWM1_BASE,EPWM_COUNTER_COMPARE_A,5000);配置波形发生器(Action Qualifier)这玩意儿跟st的hrtim里的set/reset source 差不多,就是可以对每个事件设置输出置高、置低、翻转或者无动作void EPWM_setActionQualifierAction(uint32_t base, EPWM_ActionQualifierOutputModule epwmOutput, EPWM_ActionQualifierOutput output, EPWM_ActionQualifierOutputEvent event) void EPWM_setActionQualifierActionComplete(uint32_t base, EPWM_ActionQualifierOutputModule epwmOutput, uint16_t action)Driverlib提供了两种API,上面这种可以单独设置某个事件，下面的是一次设置所有事件举个例子EPWM_setActionQualifierActionComplete(EPWM1_BASE,EPWM_AQ_OUTPUT_A,EPWM_AQ_OUTPUT_HIGH_ZERO|EPWM_AQ_OUTPUT_LOW_UP_CMPA); //////////////////////////////////////////////////////////////////////////////////////////// EPWM_setActionQualifierAction(EPWM1_BASE, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_HIGH, EPWM_AQ_OUTPUT_ON_TIMEBASE_ZERO); EPWM_setActionQualifierAction(EPWM1_BASE, EPWM_AQ_OUTPUT_A, EPWM_AQ_OUTPUT_LOW, EPWM_AQ_OUTPUT_ON_TIMEBASE_UP_CMPA);两种写法效果是一样的最后重新打开计数器的时钟SysCtl_enablePeripheral(SYSCTL_PERIPH_CLK_TBCLKSYNC);完成以上配置之后epwm模块就能输出基本的pwm波形了高精度PWM(High Resolution Pulse Width Modulation)笔者没太看懂TI这玩意儿是啥原理，手册上就说使用了一种叫MEP技术可以在一个时钟周期内插入很多个脉冲，根据数据手册的说法F28379的MEP步长一般是150ps，并且会随着温度浮动参考手册上给出了详细的配置流程，笔者依葫芦画瓢做了一遍时基单元配置首先配置重装载值void HRPWM_setTimeBasePeriod(uint32_t base, uint32_t periodCount)由于使用了高精度模式，这里的重装载值是q16.8的定点数，看函数定义可以发现这个函数是把periodcount的低八位（也就是小数部分）给了TBPRDHR,高十六位给了TBPRD比较值也是同理void HRPWM_setCounterCompareValue(uint32_t base, HRPWM_CounterCompareModule compModule, uint32_t compCount)配置计数模式和装载模式，手册说高精度模式只支持向上计数和中心对齐计数,而且必须使用影子寄存器void HRPWM_setTimeBaseCounterMode(uint32_t base, EPWM_TimeBaseCountMode counterMode) void HRPWM_setPeriodLoadMode(uint32_t base, EPWM_PeriodLoadMode loadMode)设置比较值的装载事件,注意这玩意儿只影响CMPAHRstatic inline void HRPWM_setCounterCompareShadowLoadEvent(uint32_t base, HRPWM_Channel channel,HRPWM_LoadMode loadEvent)剩下的按照一般epwm的方式配置就行，其实除了一些HRPWM特有的设置外其他API都是和EPWM一样的MEP比例因子自动转换配置开自动转换void HRPWM_enableAutoConversion(uint32_t base)不开自动转换的话MEP的比例因子就要自己算，而且由于MEP的步长会随着温度缓慢漂移，比例因子也是在缓慢发生变化的，所以还是开自动转换比较好使用自动转换还有一些额外的东西需要配置:包含必要的头文件#include "F28x7x_Device.h" // F28x7x Headerfile #include "F28x7x_EPwm_defines.h" // init defines #include "SFO_V8.h" // SFO lib functions (needed for HRPWM定义变量（注意一定要定义成全局的）int MEP_ScaleFactor = 0; //scale factor value volatile struct EPWM_REGS *ePWM[] = {0, &EPwm1Regs, &EPwm2Regs, &EPwm3Regs, &EPwm4Regs};epwm数组里放芯片各个EPWM模块的基址调用SFO()对比例因子进行初始化while (SFO() == 0) {} // MEP_ScaleFactor calculated by MEP Cal Module在后面的程序里时不时调用一下SFO()来更新比例因子(可以放在大循环里)手册里其实也给了比例因子的计算方法MEP控制配置设置MEP控制的边沿，手册让同时控制上升沿和下降沿，我也不知道为啥反正跟着做就对了HRPWM_setMEPEdgeSelect(EPWM1_BASE,HRPWM_CHANNEL_A,HRPWM_MEP_CTRL_RISING_AND_FALLING_EDGE);使能高精度控制HRPWM_enablePeriodControl(EPWM1_BASE);发送一个软件同步脉冲EPWM_forceSyncPulse(EPWM1_BASE);到这HRPWM就配置好了错误信号控制模块(Trip Zone)c2000的epwm提供了错误信号控制模块，可以从外部引脚或者软件获取错误发生的信号并按照设定在硬件层面上进行保护，还可以产生相应的中断信号方便CPU进行处理，对于电力电子这种经常要加各种保护的应用来说比较有用实际上笔者一开始是因为在c2000上找不到像st那样的IDLE电平设置才用这个来实现类似的功能保护类型错误信号控制模块提供了两种保护方式，一种叫CBC(Cycle-by-Cycle)，顾名思义就是在每个PWM周期都重置保护状态，比较适合用于过流保护之类的场景，另一种叫OSHT(One-Shot)，这种保护一旦被触发就会一直持续下去直到收到解除的信号保护动作c2000可以通过下面这个函数设置保护事件和对应的动作，保护动作有高阻态，拉高输出，拉低输出或者无动作四种void EPWM_setTripZoneAction(uint32_t base, EPWM_TripZoneEvent tzEvent, EPWM_TripZoneAction tzAction)tzEventEPWM_TZ_ACTION_EVENT_DCBEVT2 - DCBEVT2 (Digital Compare B event 2)EPWM_TZ_ACTION_EVENT_DCBEVT1 - DCBEVT1 (Digital Compare B event 1)EPWM_TZ_ACTION_EVENT_DCAEVT2 - DCAEVT2 (Digital Compare A event 2)EPWM_TZ_ACTION_EVENT_DCAEVT1 - DCAEVT1 (Digital Compare A event 1)EPWM_TZ_ACTION_EVENT_TZB - TZ1 - TZ6, DCBEVT2, DCBEVT1EPWM_TZ_ACTION_EVENT_TZA - TZ1 - TZ6, DCAEVT2, DCAEVT1tzActionEPWM_TZ_ACTION_HIGH_Z - high impedance outputEPWM_TZ_ACTION_HIGH - high outputEPWM_TZ_ACTION_LOW - low lowEPWM_TZ_ACTION_DISABLE - disable action软件触发保护void EPWM_forceTripZoneEvent(uint32_t base, uint16_t tzForceEvent)解除保护状态void EPWM_clearTripZoneFlag(uint32_t base, uint16_t tzFlags)死区发生器(Dead Band)配置死区也是电力电子经常用到的功能，下图是DB模块的框图，该模块可以分别配置上升沿和下降沿的死区时间和输出极性等从左到右每一部分在Driverlib里面都有对应的函数进行操作设置RED和FED的信号源void EPWM_setRisingEdgeDeadBandDelayInput(uint32_t base, uint16_t input) void EPWM_setFallingEdgeDeadBandDelayInput(uint32_t base, uint16_t input)EPWM_DB_INPUT_EPWMA - Input signal is ePWMA(Valid for both Falling Edge and Rising Edge)EPWM_DB_INPUT_EPWMB - Input signal is ePWMB(Valid for both Falling Edge and Rising Edge)EPWM_DB_INPUT_DB_RED - Input signal is the output of Rising Edge delay. (Valid only for Falling Edge delay)设置RED和FED的延迟时间void EPWM_setRisingEdgeDelayCount(uint32_t base, uint16_t redCount) void EPWM_setFallingEdgeDelayCount(uint32_t base, uint16_t fedCount) 设置输出极性void EPWM_setDeadBandDelayPolarity(uint32_t base, EPWM_DeadBandDelayMode delayMode, EPWM_DeadBandPolarity polarity)delayModeEPWM_DB_RED - Rising Edge delayEPWM_DB_FED - Falling Edge delaypolarityEPWM_DB_POLARITY_ACTIVE_HIGH - polarity is not inverted.EPWM_DB_POLARITY_ACTIVE_LOW - polarity is inverted.设置是否使用死区生成void EPWM_setDeadBandDelayMode(uint32_t base, EPWM_DeadBandDelayMode delayMode, bool enableDelayMode)设置输出交换（我不知道这功能应用场景是啥）void EPWM_setDeadBandOutputSwapMode(uint32_t base, EPWM_DeadBandOutput output, bool enableSwapMode)outputEPWM_DB_OUTPUT_A - ePWM output AEPWM_DB_OUTPUT_B - ePWM output BenableSwapModetrue - the output is swappedfalse - the output and the signal path are the same.手册上给了几种配置对应的输出波形，可以结合前面的框图理解如何进行时基间的同步电力电子经常涉及多个定时器之间的同步，用过stm32的应该知道它的定时器可以通过设置触发事件来实现同步，c2000的epwm也有类似的功能，但是和stm32的玩法不太一样,c2000的时基单元的同步信号是一个串一个的，可以看底下这个图考虑同步的时候可以把每个epwm模块简化成下面这样要进行同步主要要做两步，第一步是把前面的模块的同步输出信号打开,第二步是把要进行同步的模块的移相输入打开void EPWM_setSyncOutPulseMode(uint32_t base, EPWM_SyncOutPulseMode mode) void EPWM_enablePhaseShiftLoad(uint32_t base) 这样两个epwm模块就可以同步了总结本文介绍了c2000 epwm模块的基本使用方法，其实还有很多功能笔者暂时没有用上，日后用上了再回来补充，如果文章有什么错误也欢迎批评指正参考链接F28379D Driverlib API手册F28379数据手册F28379技术参考手册
前言最近在尝试把FOC部署到STM32G030上，为了使FOC的运行更加丝滑，有必要在代码上进行一些优化以加快运算速度，因此，笔者参考了St电机库的源码，从中学习到了一些优化的技巧。本文主要介绍FOC算法在St电机库中的实现方式，并与SimpleFOC进行简要对比。数据定点化众所周知，浮点数的运算相比整数运算要更耗费时间，在实际的电机控制应用中，一般会将算法中的数据变成定点数来处理（虽然SimpleFOC里面几乎全用的浮点数）。在谈论这个问题之前，我们先回顾一下浮点数和定点数在计算机中的存储方式。浮点数浮点数在内存中由三个部分组成：一个符号位、若干个指数位（表示小数点的位置），若干个尾数位。定点数定点数由于小数点的位置是固定的，因此不再需要指数位，一个定点数的小数点具体在哪个位置是由编写程序的人约定的TI的IQMath库中对定点数类型的定义SimpleFOC与St电机库数据处理的对比——以电流采样为例float _readADCVoltageInline(const int pinA, const void* cs_params){ uint32_t raw_adc = adcRead(pinA); return raw_adc * ((ESP32MCPWMCurrentSenseParams*)cs_params)->adc_voltage_conv; }可以看到SimpleFOC在电流计算的第一步就直接把原始值换算成了用浮点数表示的实际值。 bSector = pHandle->_Super.Sector; hReg1 = *pHandle->pParams_str->ADCDataReg1[bSector] * 2; hReg2 = *pHandle->pParams_str->ADCDataReg2[bSector] * 2; switch ( bSector ) { case SECTOR_4: case SECTOR_5: /* Current on Phase C is not accessible */ /* Ia = PhaseAOffset - ADC converted value) */ wAux = ( int32_t )( pHandle->PhaseAOffset ) - ( int32_t )( hReg1 ); /* Saturation of Ia */ if ( wAux < -INT16_MAX ) { pStator_Currents->a = -INT16_MAX; } else if ( wAux > INT16_MAX ) { pStator_Currents->a = INT16_MAX; } else { pStator_Currents->a = ( int16_t )wAux; }以上是三电阻电流采样函数的一部分，由于采用三电阻，因此要依电压扇区决定舍弃哪一相的电流值。主要做的事情是把原始值拿出来乘了个二然后限制了一下范围，乍一看有点摸不着头脑。根据初始化代码指定的ADC转换模式找到了对应的数据对齐方式图，可以发现乘二的操作其实就是把12位的原始值映射到了16位上（因为St电机库用的是q15的定点数）。定点数的运算定点数运算-维基百科定点数的加减法比较简单，直接按一般方法加减即可。在乘除运算时会遇到一些问题。比如两个q15的定点数相乘，要想保留全部的精度，结果应该是一个q30的数，如果想要得到一个与原来长度相同的定点数，就要舍弃一些精度来对结果进行裁剪，比如在本例中，要想让结果变成q15，需要把q30的结果右移15位。除法的规则类似。以下是St电机库中Clarke变换中定点数运算的例子： int32_t a_divSQRT3_tmp, b_divSQRT3_tmp ; int32_t wbeta_tmp; int16_t hbeta_tmp; Output.alpha = Input.a; a_divSQRT3_tmp = divSQRT_3 * ( int32_t )Input.a; b_divSQRT3_tmp = divSQRT_3 * ( int32_t )Input.b; wbeta_tmp = ( -( a_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) - ( b_divSQRT3_tmp ) ) >> 15;扇区判断在没有加入电流环时，我们无法对U轴和D轴的电流进行精确的控制，输出电压的D分量一般设为0，此时电压矢量的扇区可以直接由转子机械角度换算得到，但在加入电流环之后，D分量不一定为零，因此需要针对输出电压矢量重新判断扇区。SimpleFOC的方案SimpleFOC新版本加入了电流环的功能，因此它里面也有这部分的代码。它对电压矢量扇区判断的操作非常直球，既然加入D分量会使得电角度相对转子角度发生偏移，那我把这个偏移的角度加上去再判断不就完事了吗，于是就有了下面的代码：if(Ud){ // only if Ud and Uq set // _sqrt is an approx of sqrt (3-4% error) Uout = _sqrt(Ud*Ud + Uq*Uq) / driver->voltage_limit; // angle normalisation in between 0 and 2pi // only necessary if using _sin and _cos - approximation functions angle_el = _normalizeAngle(angle_el + atan2(Uq, Ud)); }else{// only Uq available - no need for atan2 and sqrt Uout = Uq / driver->voltage_limit; // angle normalisation in between 0 and 2pi // only necessary if using _sin and _cos - approximation functions angle_el = _normalizeAngle(angle_el + _PI_2); }可以看到它计算这个偏移角度是通过atan2函数实现的，在效率上还是会打点折扣，那么有没有优雅一点的方法呢？St电机库的方案(其实这个方案不止St在用，大部分正经FOC的扇区判断基本上都是这样算的)首先将PI控制器输出的两个分量进行反Park变换得到α和β分量，然后通过以下三个式子各自的正负就能判断扇区：为什么用这仨玩意儿就能判断出扇区呢，我们把这几个式子稍微变一下形就很直观了。已知：那上面的式子就可以如下表达：是不是就一目了然了，X划分的是上半和下半的扇区，Y划分的是左上和右下的扇区，Z划分的是右上和左下的扇区，确定了这三个量，扇区自然就能确定了。对照一下St的源码： wUAlpha = Valfa_beta.alpha * ( int32_t )pHandle->hT_Sqrt3; wUBeta = -( Valfa_beta.beta * ( int32_t )( pHandle->PWMperiod ) ) * 2; wX = wUBeta; wY = ( wUBeta + wUAlpha ) / 2; wZ = ( wUBeta - wUAlpha ) / 2;这里只是进行了一些等比例的缩放，方便后续使用，道理是一样的。这三个量不仅用于判断扇区，还可以直接用于输出占空比的计算，首先回顾一下一个周期内两个矢量持续时间的计算公式（以扇区I为例）：是不是看起来有点眼熟，其实就是上面那三个玩意儿凑的，其它扇区也是同理。下面是St电机库计算占空比的部分代码：if ( wY < 0 ) { if ( wZ < 0 ) { pHandle->Sector = SECTOR_5; wTimePhA = ( int32_t )( pHandle->PWMperiod ) / 4 + ( ( wY - wZ ) / ( int32_t )262144 ); wTimePhB = wTimePhA + wZ / 131072; wTimePhC = wTimePhA - wY / 131072; pHandle->lowDuty = wTimePhC; pHandle->midDuty = wTimePhA; pHandle->highDuty = wTimePhB; }可以看到最终输出占空比可以由这三个量计算得到，至于底下除的那个131072，我也不是很明白是怎么来的，但这只是对数据进行了一些缩放来匹配定时器，基本思路是很清楚的。磁链圆限制关于输出电压矢量最大是√3/2Udc这码事不少人应该都看过这个图，图中这个六边形是输出的最大电压范围，这是由于一个周期内Tx和Ty之和是不可能大于T的，它的内切圆是能让转矩稳定输出的最大电压范围，当电压矢量超出这个圆时，某些位置的电压超出了六边形，逆变器输出不了，就会造成电机的转矩不稳定，这一现象也被称为过调制。将Tx+Ty<=T这个条件代入SVPWM的公式中是可以求出Uref的范围的：√3/2Udc就是这个内切圆的半径。在实际的应用中，考虑到管子的开关时间和电流采样等因素，这个圆还要再小一点。但PI控制器输出的电压矢量是有可能超出这个圆的，遇到这种情况时，就需要对矢量进行缩小，将其限制在圆的范围内。缩小电压矢量的方法要把电压矢量缩到圆内，只需要将q和d分量缩小一定倍数即可，设q和d分量的缩小系数为k，有：式中出现的根号运算也比较费时，因此St使用查表的方式来求取这个系数。__weak qd_t Circle_Limitation( CircleLimitation_Handle_t * pHandle, qd_t Vqd ) { uint16_t table_element; uint32_t uw_temp; int32_t sw_temp; qd_t local_vqd = Vqd; sw_temp = ( int32_t )( Vqd.q ) * Vqd.q + ( int32_t )( Vqd.d ) * Vqd.d; uw_temp = ( uint32_t ) sw_temp; /* uw_temp min value 0, max value 32767*32767 */ if ( uw_temp > ( uint32_t )( pHandle->MaxModule ) * pHandle->MaxModule ) { uw_temp /= ( uint32_t )( 16777216 ); /* wtemp min value pHandle->Start_index, max value 127 */ uw_temp -= pHandle->Start_index; /* uw_temp min value 0, max value 127 - pHandle->Start_index */ table_element = pHandle->Circle_limit_table[( uint8_t )uw_temp]; sw_temp = Vqd.q * ( int32_t )table_element; local_vqd.q = ( int16_t )( sw_temp / 32768 ); sw_temp = Vqd.d * ( int32_t )( table_element ); local_vqd.d = ( int16_t )( sw_temp / 32768 ); } return ( local_vqd ); }参考文章ST电机库v5.4.4源代码分析(3): α、β方向的电流值与三相PWM 波形的联系FOC中的SVPWM原理细讲
前言学校电赛冬令营发了一个带传感器的无刷云台电机和一块simpleFOC驱动板，借此机会学习一下有感FOC的基本原理和使用。前置知识稚晖君这篇文章里讲得非常的简单易懂，推荐阅读：https://zhuanlan.zhihu.com/p/147659820笔者在此对文章中的内容作简要的总结。FOC简介FOC全称Field-Oriented Control——磁场定向控制,FOC可以适用在交流感应电机及直流无刷电机，早期开发的目的为了高性能的电机应用，可以在整个频率范围内运转、电机零速时可以输出额定转矩、且可以快速的加减速。正因为上述的各种优点，如今FOC被广泛地用于各种机器人系统的电机驱动中。Clark变换和Park变换Clark变换是将三相电机的电流、电压向量进行了正交化；Park令两个正交的向量跟随转子进行旋转（对于这一步，个人理解是当向量随着转子旋转的时候，转子受到的力矩全部来源于两个向量中的q分量，因此只需控制q分量的大小就可以控制当前电机的转矩，并且可以通过减小d分量来减少其它的能量损耗）。对于这两步变换的作用，笔者认为稚晖君做的总结非常到位：所谓的“矢量控制”其实就是在做解耦，把相互耦合的三相磁链解耦为容易控制的交轴和直轴。整个过程就好比我们在做信号处理的时候，通过FFT把信号变换到频域进行处理之后再IFFT反变换回时域是一个道理SVPWM技术掌握了无刷电机输入相量的简化方法后，我们需要考虑如何进行输出，即如何将解耦后的交轴和直轴分量重新转化为电机的三个相量。通过改变半桥开关管的通断，三相电机的三个绕组共可以产生六种不同方向的磁场。而要产生任意方向任意强度的磁场，我们需要将该方向所处扇区两边的两个矢量进行合成，这一步合成的操作就需要使用SVPWM（Space Vector Pulse Width Modulation——空间矢量脉宽调制)技术。与我们熟悉的PWM技术不同的是，SVPWM从原本的一维标量变成了二维的矢量，它利用了电机绕组的电感特性对三个磁链（第三个是0）进行合成，实现了任意方向任意强度磁场的产生。具体实现方法见稚晖君文章。机械角度与电角度要实现以上操作，有一点我们不能忽略，就是我们需要知道转子当前的位置。在有感FOC中，转子的位置是通过角度传感器获取的，SimpleFOC支持四种不同类型的角度传感器。在极对数为一的情况下，电机的机械角度和计算电压矢量时使用的角度是可以完全对应的，但是在实际的无刷电机中，极对数一般会更多：这时候就出现了一个问题——电机的机械角度和电压矢量的角度是不相等的。从上图可以很直观地看出，在极对数为二的电机中，在电压矢量旋转了一圈后，电机实际上只转了半圈。所以要得到电角度，需要将传感器测得的机械角度进行一些换算，公式非常简单：电角度=机械角度*极对数程序分析SVPWMSimpleFOC代码最核心的部分，实际上是SVPWM的实现，因此要将SimpleFOC移植至不同平台，基本上就是移植这一部分的代码。 float Uout; if(Ud){ // 设置了不为零的d分量的情况，如果是带电流环的时候d分量可能会有输出 Uout = _sqrt(Ud*Ud + Uq*Uq) / driver->voltage_limit;//计算矢量的大小 angle_el = _normalizeAngle(angle_el + atan2(Uq, Ud));//计算矢量的方向 }else{//d分量为零的情况 Uout = Uq / driver->voltage_limit; angle_el = _normalizeAngle(angle_el + _PI_2);//将转子角度归一化至0到2pi之间 } sector = floor(angle_el / _PI_3) + 1;//求当前所处扇区 //根据SVPWM公式计算时间 float T1 = _SQRT3*_sin(sector*_PI_3 - angle_el) * Uout; float T2 = _SQRT3*_sin(angle_el - (sector-1.0f)*_PI_3) * Uout; float T0 = 0; //开发者说在供电电压比较低的时候可以不使用中心对齐，把零矢量的时间设为零 if (modulation_centered) { T0 = 1 - T1 - T2; //当使用PWM中心对齐模式输出时计算零矢量的持续时间 } // 根据转子所在扇区计算三个相位的时间（七段式SVPWM） float Ta,Tb,Tc; switch(sector){ case 1: Ta = T1 + T2 + T0/2; Tb = T2 + T0/2; Tc = T0/2; break; case 2: Ta = T1 + T0/2; Tb = T1 + T2 + T0/2; Tc = T0/2; break; case 3: Ta = T0/2; Tb = T1 + T2 + T0/2; Tc = T2 + T0/2; break; case 4: Ta = T0/2; Tb = T1+ T0/2; Tc = T1 + T2 + T0/2; break; case 5: Ta = T2 + T0/2; Tb = T0/2; Tc = T1 + T2 + T0/2; break; case 6: Ta = T1 + T2 + T0/2; Tb = T0/2; Tc = T1 + T0/2; break; default: Ta = 0; Tb = 0; Tc = 0; } //计算相电压 Ua = Ta*driver->voltage_limit; Ub = Tb*driver->voltage_limit; Uc = Tc*driver->voltage_limit; break; } //输出 driver->setPwm(Ua, Ub, Uc);零点检测为了实现机械角度和电角度零点的对齐，我们需要先进行零点位置的检测。 setPhaseVoltage(voltage_sensor_align, 0, _3PI_2);//产生电角度为3π/2的磁场，把转子拖过去 _delay(700);//等待转子到位 //读取机械角度 sensor->update(); //根据极对数和机械角度计算出电角度的零点 zero_electric_angle = 0; zero_electric_angle = electricalAngle(); //zero_electric_angle = _normalizeAngle(_electricalAngle(sensor_direction*sensor->getAngle(), pole_pairs)); _delay(20); // stop everything setPhaseVoltage(0, 0, 0); _delay(200);移植注意事项1.配置PWM时记得设为中心对齐，并且注意每个通道占空比和电压保持时间的关系（笔者写反了一次），设置正确后三个通道产生的波形大概长这样： 其实认真观察可以看到占空比的变化就是一个马鞍波2.程序中的所有角度量都是弧度制，范围是0~2π。对于使用I2C协议的磁编码器，SimpleFOC库中有AS5600和AS5048两款芯片的驱动代码，直接搬过来用即可。3.注意传感器角度和电压矢量旋转的方向要一致。SimpleFOC也提供了自动检测传感器安装方向和极对数的程序，笔者移植的时候偷懒直接写死了。 if(!_isset(sensor_direction)){ // find natural direction // move one electrical revolution forward for (int i = 0; i <=500; i++ ) { float angle = _3PI_2 + _2PI * i / 500.0f; setPhaseVoltage(voltage_sensor_align, 0, angle); sensor->update(); _delay(2); } // take and angle in the middle sensor->update(); float mid_angle = sensor->getAngle(); // move one electrical revolution backwards for (int i = 500; i >=0; i-- ) { float angle = _3PI_2 + _2PI * i / 500.0f ; setPhaseVoltage(voltage_sensor_align, 0, angle); sensor->update(); _delay(2); } sensor->update(); float end_angle = sensor->getAngle(); setPhaseVoltage(0, 0, 0); _delay(200); // determine the direction the sensor moved if (mid_angle == end_angle) { SIMPLEFOC_DEBUG("MOT: Failed to notice movement"); return 0; // failed calibration } else if (mid_angle < end_angle) { SIMPLEFOC_DEBUG("MOT: sensor_direction==CCW"); sensor_direction = Direction::CCW; } else{ SIMPLEFOC_DEBUG("MOT: sensor_direction==CW"); sensor_direction = Direction::CW; } // check pole pair number float moved = fabs(mid_angle - end_angle); if( fabs(moved*pole_pairs - _2PI) > 0.5f ) { // 0.5f is arbitrary number it can be lower or higher! SIMPLEFOC_DEBUG("MOT: PP check: fail - estimated pp: ", _2PI/moved); } else SIMPLEFOC_DEBUG("MOT: PP check: OK!"); } else SIMPLEFOC_DEBUG("MOT: Skip dir calib."); 总结经过本次SimpleFOC的移植，笔者掌握了有感FOC的基本原理，并且利用FOC实现了一些如扭矩控制，位置控制的简单功能。可惜的是，手里这块驱动板是一块不带电流反馈的简易版驱动（SimpleFOC貌似也是不久前才增加的电流环功能），因此无法做到扭矩的精确控制，笔者计划之后自行设计一块能够进行电流闭环的驱动板。附录参考资料：Arduino Simple Field Oriented Control (FOC) project【自制FOC驱动器】深入浅出讲解FOC算法与SVPWM技术关于矢量控制clarke变化2/3 和sqrt（2/3）的理解《DMF407电机控制专题教程》第27章 FOC
简介FSMC(Flexible Static Memory Controller，可变静态存储控制器)是STM32系列采用的一种新型的存储器扩展技术。在外部存储器扩展方面具有独特的优势，可根据系统的应用需要，方便地进行不同类型大容量静态存储器的扩展（还有一种用途是用来驱动并口屏）系统框图*本文使用的图片均出自STM32H750VB的参考手册，FMC的具体情况在不同型号的芯片内可能会有所不同。在STM32单片机中，FMC的作用相当于沟通了芯片内部的设备总线和外部存储器件，使得CPU能够通过总线访问到外部器件的内存。内存映射FMC内存框图FMC的内存被分为若干个内存块，其中内存块1拥有一个独立的2位片选信号，将该内存块分为了四个扇区，也就是支持最多连接四个内存设备。NOR/PSRAM设备的内存映射前文提到内存块1的2位片选信号由地址线的第27、28位控制。地址线的前26位用于访问外部内存设备中的指定位置。考虑到设备寄存器宽度不同，st官方给出了以上说明。在对外部通信过程中，若设备的寄存器宽度为16位，那么地址线的第26位将不被使用，32位寄存器同理，不使用第26、25位。同时，对于CPU而言，内存的单位仍然是一个字节，因此在使用外部设备的寄存器地址时应当将其地址左移相应位数。应用举例:ILI9341LCD驱动芯片ILI9341使用8080通信协议，STM32的FSMC使用该协议时要求用户指定地址线上的一位作为8080的数据、命令信号。实际上是将LCD当作了一种只有两个寄存器的SRAM芯片，从而将SRAM总线的通信转换成了8080.以下是正点原子写的驱动代码：//LCD地址结构体 typedef struct { u16 LCD_REG; u16 LCD_RAM; } LCD_TypeDef; //使用NOR/SRAM的 Bank1.sector1,地址位HADDR[27,26]=00 A18作为数据命令区分线 //注意设置时STM32内部会右移一位对齐! #define LCD_BASE ((u32)(0x60000000 | 0x00007FFFE)) #define LCD ((LCD_TypeDef *) LCD_BASE)结合上文不难得知LCD内存的“基地址”如何求取。
前言前段时间购入了一块MATEK的传感器模块，上面包含了一个光流和一个激光传感器，该模块使用MSP通信协议与飞控进行串口通信，因此笔者尝试从INAV的开源飞控代码入手研究它的具体通信过程协议介绍官方文档地址：https://github.com/iNavFlight/inav/wiki/MSP-V2协议简介MSP is a request-response protocol. MSP defines the side sending the request as "MSP Master" role and the side generating a response as "MSP Slave" role.A specific device may function as both "MSP Master" and "MSP Slave" or implement only one role, depending on requirements. Generally, a Groundstation software is an "MSP Master" and FC is an "MSP Slave", however in some use-cases a FC or any other device may be required to act as an "MSP Master" and "MSP Slave" concurrently.简单浏览得知，飞控和外设之间通信是经典的主从关系报文格式根据官方给出的表格：和一般的通信报文一样，MSP的报文中也包含了固定的帧头以及末尾的校验和。第三到第六字节包含了一些与本次通信相关的信息，其中type根据官方文档一共有三种，分别是请求、应答和异常：function包含了本次通信的功能信息，以传感器为例，传感器发出的MSP报文的function分为以下几类：MSP2_SENSOR_RANGEFINDER 0x1F01 //距离传感器 MSP2_SENSOR_OPTIC_FLOW 0x1F02 //光流传感器 MSP2_SENSOR_GPS 0x1F03 //GPS MSP2_SENSOR_COMPASS 0x1F04 //指南针 MSP2_SENSOR_BAROMETER 0x1F05 //气压计 MSP2_SENSOR_AIRSPEED 0x1F06 //空速计程序框架INAV飞控MSP处理程序的结构如下所示：mspSerialProcessmspSerialrocessOneportserialReadmspSerialProcessReceivedDatamspSerialProcessReceivedCommand在fc_tasks.c中，串口的处理作为一个任务交给系统调度，其中进行MSP通信处理的是下面的函数，该函数的两个参数分别是非MSP报文的处理方式以及报文处理函数的函数指针（记住，后面要考）：// Allow MSP processing even if in CLI mode mspSerialProcess(ARMING_FLAG(ARMED) ? MSP_SKIP_NON_MSP_DATA : MSP_EVALUATE_NON_MSP_DATA, mspFcProcessCommand);mspSerialProcessvoid mspSerialProcess(mspEvaluateNonMspData_e evaluateNonMspData, mspProcessCommandFnPtr mspProcessCommandFn) { for (uint8_t portIndex = 0; portIndex < MAX_MSP_PORT_COUNT; portIndex++) { mspPort_t * const mspPort = &mspPorts[portIndex]; if (mspPort->port) { mspSerialProcessOnePort(mspPort, evaluateNonMspData, mspProcessCommandFn); } } }该函数按次序将所有可用的msp端口进行处理，可以观察到该函数将上一层的参数都传了进去，在此基础上增加了所用端口的信息。mspSerialrocessOneportvoid mspSerialProcessOnePort(mspPort_t * const mspPort, mspEvaluateNonMspData_e evaluateNonMspData, mspProcessCommandFnPtr mspProcessCommandFn) { mspPostProcessFnPtr mspPostProcessFn = NULL; if (serialRxBytesWaiting(mspPort->port)) { // There are bytes incoming - abort pending request mspPort->lastActivityMs = millis(); mspPort->pendingRequest = MSP_PENDING_NONE; // Process incoming bytes while (serialRxBytesWaiting(mspPort->port)) { const uint8_t c = serialRead(mspPort->port); const bool consumed = mspSerialProcessReceivedData(mspPort, c); if (!consumed && evaluateNonMspData == MSP_EVALUATE_NON_MSP_DATA) { mspEvaluateNonMspData(mspPort, c); } if (mspPort->c_state == MSP_COMMAND_RECEIVED) { mspPostProcessFn = mspSerialProcessReceivedCommand(mspPort, mspProcessCommandFn); break; // process one command at a time so as not to block. } } if (mspPostProcessFn) { waitForSerialPortToFinishTransmitting(mspPort->port); mspPostProcessFn(mspPort->port); } } else { mspProcessPendingRequest(mspPort); } }可以观察到该函数主要完成三个工作，一是读取完整的报文，二是进行协议的解析，三是对报文内容进行处理，分别对应前文程序框架中提到的三个函数。报文读取：serialReaduint8_t serialRead(serialPort_t *instance) { return instance->vTable->serialRead(instance); }实际上是调用了端口对应的读取函数，以使用了STM32的UART的端口的情况为例，这一操作调用的实际上是以下函数：uint8_t uartRead(serialPort_t *instance) { uint8_t ch; uartPort_t *s = (uartPort_t *)instance; ch = s->port.rxBuffer[s->port.rxBufferTail]; if (s->port.rxBufferTail + 1 >= s->port.rxBufferSize) { s->port.rxBufferTail = 0; } else { s->port.rxBufferTail++; } return ch; }封装技巧：开发者在这里使用了一种将函数指针封装在结构体内的方法，移植时只需要将对应的函数指针赋值即可，这样便于针对多种不同硬件同时使用不同配置，也避免了使用者修改c文件中的内容（大概）struct serialPortVTable { void (*serialWrite)(serialPort_t *instance, uint8_t ch); uint32_t (*serialTotalRxWaiting)(const serialPort_t *instance); uint32_t (*serialTotalTxFree)(const serialPort_t *instance); uint8_t (*serialRead)(serialPort_t *instance); // Specified baud rate may not be allowed by an implementation, use serialGetBaudRate to determine actual baud rate in use. void (*serialSetBaudRate)(serialPort_t *instance, uint32_t baudRate); bool (*isSerialTransmitBufferEmpty)(const serialPort_t *instance); void (*setMode)(serialPort_t *instance, portMode_t mode); void (*writeBuf)(serialPort_t *instance, const void *data, int count); bool (*isConnected)(const serialPort_t *instance); bool (*isIdle)(serialPort_t *instance); // Optional functions used to buffer large writes. void (*beginWrite)(serialPort_t *instance); void (*endWrite)(serialPort_t *instance); };协议解析：mspSerialProcessReceivedDataINAV对于协议的解析是通过一个有限状态机来完成的，其中包括帧头的识别和接收完成后的校验，状态转移具体见下图：完成接收后，该函数会将报文存储到对应端口的buffer，并且返回一个COMMAND_RECEIVED，表示接收完成，之后串口处理函数进入下一步操作。由于这一部分源码实在是太大一坨，为了观看体验就不放在这里了，感兴趣的同学可以划到最底下看一看。内容处理：mspSerialProcessReceivedCommandstatic mspPostProcessFnPtr mspSerialProcessReceivedCommand(mspPort_t *msp, mspProcessCommandFnPtr mspProcessCommandFn) { uint8_t outBuf[MSP_PORT_OUTBUF_SIZE]; mspPacket_t reply = { .buf = { .ptr = outBuf, .end = ARRAYEND(outBuf), }, .cmd = -1, .flags = 0, .result = 0, }; uint8_t *outBufHead = reply.buf.ptr; mspPacket_t command = { .buf = { .ptr = msp->inBuf, .end = msp->inBuf + msp->dataSize, }, .cmd = msp->cmdMSP, .flags = msp->cmdFlags, .result = 0, }; mspPostProcessFnPtr mspPostProcessFn = NULL; const mspResult_e status = mspProcessCommandFn(&command, &reply, &mspPostProcessFn); if (status != MSP_RESULT_NO_REPLY) { sbufSwitchToReader(&reply.buf, outBufHead); // change streambuf direction mspSerialEncode(msp, &reply, msp->mspVersion); } msp->c_state = MSP_IDLE; return mspPostProcessFn; }除去前面的一些初始化代码以及一些后续的处理，这段代码的核心其实就是mspProcessCommandFn这个函数，看到这里是不是觉得有点眼熟？没错，那个从一开始就出现的函数指针到这里终于被调用了，我们来康康里面到底是什么东西。mspResult_e mspFcProcessCommand(mspPacket_t *cmd, mspPacket_t *reply, mspPostProcessFnPtr *mspPostProcessFn) { mspResult_e ret = MSP_RESULT_ACK; sbuf_t *dst = &reply->buf; sbuf_t *src = &cmd->buf; const uint16_t cmdMSP = cmd->cmd; // initialize reply by default reply->cmd = cmd->cmd; if (MSP2_IS_SENSOR_MESSAGE(cmdMSP)) { ret = mspProcessSensorCommand(cmdMSP, src); } else if (mspFcProcessOutCommand(cmdMSP, dst, mspPostProcessFn)) { ret = MSP_RESULT_ACK; } else if (cmdMSP == MSP_SET_PASSTHROUGH) { mspFcSetPassthroughCommand(dst, src, mspPostProcessFn); ret = MSP_RESULT_ACK; } else { if (!mspFCProcessInOutCommand(cmdMSP, dst, src, &ret)) { ret = mspFcProcessInCommand(cmdMSP, src); } } // Process DONT_REPLY flag if (cmd->flags & MSP_FLAG_DONT_REPLY) { ret = MSP_RESULT_NO_REPLY; } reply->result = ret; return ret; }这个函数判定报文的类型，再根据报文类型作相应的处理。报文类型判定可以看到，报文类型判定时访问的是cmdMSP这个变量，我们来看看这个变量最初是在哪里被赋值的呢？case MSP_HEADER_V2_NATIVE: mspPort->inBuf[mspPort->offset++] = c; mspPort->checksum2 = crc8_dvb_s2(mspPort->checksum2, c); if (mspPort->offset == sizeof(mspHeaderV2_t)) { mspHeaderV2_t * hdrv2 = (mspHeaderV2_t *)&mspPort->inBuf[0]; // Check for potential buffer overflow if (hdrv2->size > MSP_PORT_INBUF_SIZE) { mspPort->c_state = MSP_IDLE; } else { mspPort->dataSize = hdrv2->size; mspPort->cmdMSP = hdrv2->cmd; mspPort->cmdFlags = hdrv2->flags; mspPort->offset = 0; // re-use buffer mspPort->c_state = mspPort->dataSize > 0 ? MSP_PAYLOAD_V2_NATIVE : MSP_CHECKSUM_V2_NATIVE; } } break;顺藤摸瓜，发现它的赋值是在mspSerialProcessReceivedData中完成的，赋值的内容实际上就是报文中那两个字节的function。由于这次使用的是传感器模块，我们来看一下传感器判断的宏里面是什么：#define MSP2_IS_SENSOR_MESSAGE(x) ((x) >= 0x1F00 && (x) <= 0x1FFF) #define MSP2_SENSOR_RANGEFINDER 0x1F01 #define MSP2_SENSOR_OPTIC_FLOW 0x1F02 #define MSP2_SENSOR_GPS 0x1F03 #define MSP2_SENSOR_COMPASS 0x1F04 #define MSP2_SENSOR_BAROMETER 0x1F05 #define MSP2_SENSOR_AIRSPEED 0x1F06对应上了我们之前提到的传感器报文类型。在这之后就是许多不同类型报文的具体处理了，这里我们不继续展开。封装技巧：开发者在这里也利用了结构体来进行报文的解析，以上文代码中的hdrv2结构体为例：typedef struct __attribute__((packed)) { uint8_t flags; uint16_t cmd; uint16_t size; } mspHeaderV2_t;结合上文我们不难得知，将报文缓冲区的指针赋值给该结构体类型的指针后，我们就可以以访问结构体成员的方式读取报文对应位置的信息，增强了代码的可读性。与笔者平时惯用的移位后赋值给对应变量的写法相比也节省了部分内存，提高了程序的效率。总结通过对INAV飞控MSP协议通信程序的分析，笔者更加具体地了解了协议手册中未提及的内容，从中也学习了一些程序封装的技巧，最重要的是我终于可以把模块牛过来自己用了（雾）附录参考资料：INAV飞控源码MSP协议手册BetaFlight模块设计之三十二：MSP协议模块分析mspSerialProcessReceivedData源码：static bool mspSerialProcessReceivedData(mspPort_t *mspPort, uint8_t c) { switch (mspPort->c_state) { default: case MSP_IDLE: // Waiting for '$' character if (c == '$') { mspPort->mspVersion = MSP_V1; mspPort->c_state = MSP_HEADER_START; } else { return false; } break; case MSP_HEADER_START: // Waiting for 'M' (MSPv1 / MSPv2_over_v1) or 'X' (MSPv2 native) switch (c) { case 'M': mspPort->c_state = MSP_HEADER_M; break; case 'X': mspPort->c_state = MSP_HEADER_X; break; default: mspPort->c_state = MSP_IDLE; break; } break; case MSP_HEADER_M: // Waiting for '<' if (c == '<') { mspPort->offset = 0; mspPort->checksum1 = 0; mspPort->checksum2 = 0; mspPort->c_state = MSP_HEADER_V1; } else { mspPort->c_state = MSP_IDLE; } break; case MSP_HEADER_X: if (c == '<') { mspPort->offset = 0; mspPort->checksum2 = 0; mspPort->mspVersion = MSP_V2_NATIVE; mspPort->c_state = MSP_HEADER_V2_NATIVE; } else { mspPort->c_state = MSP_IDLE; } break; case MSP_HEADER_V1: // Now receive v1 header (size/cmd), this is already checksummable mspPort->inBuf[mspPort->offset++] = c; mspPort->checksum1 ^= c; if (mspPort->offset == sizeof(mspHeaderV1_t)) { mspHeaderV1_t * hdr = (mspHeaderV1_t *)&mspPort->inBuf[0]; // Check incoming buffer size limit if (hdr->size > MSP_PORT_INBUF_SIZE) { mspPort->c_state = MSP_IDLE; } else if (hdr->cmd == MSP_V2_FRAME_ID) { // MSPv1 payload must be big enough to hold V2 header + extra checksum if (hdr->size >= sizeof(mspHeaderV2_t) + 1) { mspPort->mspVersion = MSP_V2_OVER_V1; mspPort->c_state = MSP_HEADER_V2_OVER_V1; } else { mspPort->c_state = MSP_IDLE; } } else { mspPort->dataSize = hdr->size; mspPort->cmdMSP = hdr->cmd; mspPort->cmdFlags = 0; mspPort->offset = 0; // re-use buffer mspPort->c_state = mspPort->dataSize > 0 ? MSP_PAYLOAD_V1 : MSP_CHECKSUM_V1; // If no payload - jump to checksum byte } } break; case MSP_PAYLOAD_V1: mspPort->inBuf[mspPort->offset++] = c; mspPort->checksum1 ^= c; if (mspPort->offset == mspPort->dataSize) { mspPort->c_state = MSP_CHECKSUM_V1; } break; case MSP_CHECKSUM_V1: if (mspPort->checksum1 == c) { mspPort->c_state = MSP_COMMAND_RECEIVED; } else { mspPort->c_state = MSP_IDLE; } break; case MSP_HEADER_V2_OVER_V1: // V2 header is part of V1 payload - we need to calculate both checksums now mspPort->inBuf[mspPort->offset++] = c; mspPort->checksum1 ^= c; mspPort->checksum2 = crc8_dvb_s2(mspPort->checksum2, c); if (mspPort->offset == (sizeof(mspHeaderV2_t) + sizeof(mspHeaderV1_t))) { mspHeaderV2_t * hdrv2 = (mspHeaderV2_t *)&mspPort->inBuf[sizeof(mspHeaderV1_t)]; mspPort->dataSize = hdrv2->size; // Check for potential buffer overflow if (hdrv2->size > MSP_PORT_INBUF_SIZE) { mspPort->c_state = MSP_IDLE; } else { mspPort->cmdMSP = hdrv2->cmd; mspPort->cmdFlags = hdrv2->flags; mspPort->offset = 0; // re-use buffer mspPort->c_state = mspPort->dataSize > 0 ? MSP_PAYLOAD_V2_OVER_V1 : MSP_CHECKSUM_V2_OVER_V1; } } break; case MSP_PAYLOAD_V2_OVER_V1: mspPort->checksum2 = crc8_dvb_s2(mspPort->checksum2, c); mspPort->checksum1 ^= c; mspPort->inBuf[mspPort->offset++] = c; if (mspPort->offset == mspPort->dataSize) { mspPort->c_state = MSP_CHECKSUM_V2_OVER_V1; } break; case MSP_CHECKSUM_V2_OVER_V1: mspPort->checksum1 ^= c; if (mspPort->checksum2 == c) { mspPort->c_state = MSP_CHECKSUM_V1; // Checksum 2 correct - verify v1 checksum } else { mspPort->c_state = MSP_IDLE; } break; case MSP_HEADER_V2_NATIVE: mspPort->inBuf[mspPort->offset++] = c; mspPort->checksum2 = crc8_dvb_s2(mspPort->checksum2, c); if (mspPort->offset == sizeof(mspHeaderV2_t)) { mspHeaderV2_t * hdrv2 = (mspHeaderV2_t *)&mspPort->inBuf[0]; // Check for potential buffer overflow if (hdrv2->size > MSP_PORT_INBUF_SIZE) { mspPort->c_state = MSP_IDLE; } else { mspPort->dataSize = hdrv2->size; mspPort->cmdMSP = hdrv2->cmd; mspPort->cmdFlags = hdrv2->flags; mspPort->offset = 0; // re-use buffer mspPort->c_state = mspPort->dataSize > 0 ? MSP_PAYLOAD_V2_NATIVE : MSP_CHECKSUM_V2_NATIVE; } } break; case MSP_PAYLOAD_V2_NATIVE: mspPort->checksum2 = crc8_dvb_s2(mspPort->checksum2, c); mspPort->inBuf[mspPort->offset++] = c; if (mspPort->offset == mspPort->dataSize) { mspPort->c_state = MSP_CHECKSUM_V2_NATIVE; } break; case MSP_CHECKSUM_V2_NATIVE: if (mspPort->checksum2 == c) { mspPort->c_state = MSP_COMMAND_RECEIVED; } else { mspPort->c_state = MSP_IDLE; } break; } return true; }
安装CLion1.进入Clion官网下载CLion(如果不知道如何获取证书的可以看底下的附录)2.安装Clion安装openocdopocd下载链接：https://gnutoolchains.com/arm-eabi/openocd/1.解压压缩包2.添加环境变量​ (1)打开设置-系统-系统信息-高级系统设置​ (2)点击环境变量​ ​ (3)向用户变量-path中添加第一步解压出的bin文件夹安装gcc1.打开gcc安装器2.安装​ (1)设置语言​ (2)阅读协议​ (3)设置安装路径​ (4)等待安装完成3.安装完成后勾选添加环境变量选项​ 安装调试器驱动1.下载Stellaris ICDI驱动下载链接：https://www.ti.com.cn/tool/cn/STELLARIS_ICDI_DRIVERS2.打开设备管理器，找到调试器3.安装调试器驱动​ 4.安装完成​ 配置CLion1.打开设置2.选择“嵌入式开发”，设置OpenOCD和Stm32CubeMX的路径3.选择编辑、调试选项4.选择对应的配置文件完成上述操作后，你就可以愉快地使用CLion进行TM4的开发了附：JetBrains全家桶申请方法1.进入JetBrains官网的申请界面https://www.jetbrains.com/shop/eform/students2.填写信息（注意邮箱使用教育邮箱)3.收取验证邮件4.开始白嫖