前言:
i.MX8M Plus 开发板是一款拥有 4 个 Cortex-A53 核心,运行频率 1.8GHz;1 个 Cortex-M7 核心,运行频率 800MHz;此外还集成了一个 2.3 TOPS 的 NPU,大大加速机器学习推理。
全文所使用的开发平台均为与NXP官方合作的FS-IMX8MPCA开发板(华清远见imx8mp开发板),支持Weston、ubuntu20.04、Android11 等操作系统;同时支持 Xenomai 硬实时内核、EtherCAT 总线、TSN 时间敏感网络、ROS1.0、ROS2.0 等工业与机器人领域应用;可以用于工业互联网、人工智能、边缘计算、多屏异显等应用方向。华清远见研发中心编写了大量开发教程并录制了丰富视频教学资源免费提供给大家!
开发板更多资料可关注华清远见在线实验室(微信号:hqyjlab)领取``
Cortex-M7 开发
第一个程序 Helloworld
本章节以最简单的 Helloworld 程序为例,在开发板上 Cortex-M7 核心上运行该程序,来
演示程序的调试,编译和下载。
Cortex-M7 程序编译与运行
在启动 Cortex-M7 核心之前需要先在开发板连接调试串口以及开发板电源。
⚫ 导入源码
首先需要打开基于 Cortex-M7 的 Helloworld 程序,该程序位于【华清远见-I.MX8M Plus
开发资料\程序源码\Cortex-M7】下的 SDK_2_10_0_EVK-MIMX8MP.zip 文件夹。
打开“IAR EW for Arm 9.10.1”应用,依次点击“File”->“Open workspace…”按钮
在弹出的对话框中选择 SDK_2_10_0_EVKMIMX8MP/boards/evkmimx8mp/demo_apps/helloworld\iar 目录下的 hello_world.eww 文件即可
打开 hello_world 工程。
之后可以在窗口的左上角看到 hello_world 工程
⚫ 编译源码
hello_world 工程的结构,分为“debug”,“release”,“ddr_debug”,“ddr_release”,
“flash_debug”,“flash_release”。
debug/release:程序用于在 CPU 内部的 TCM 中执行。
ddr_debug/ddr_release:主要用于在 DRAM 中执行。
flash_debug/ flash_release:主要用于在 QSPI/XIP 部存储器中执行。
这里我们通过仿真器对程序进行仿真,因此使用 debug/release 代码进行编译。
在窗口左上角的 Workspace 区域找到 dubug 菜单
切换成功后,在工具栏中找到“Make”按钮对程序镜像编译。
编译成功如下图所示
Cortex-M7 协处理器的启动
在启动 Cortex-M7 核心之前需要先在开发板连接调试串口以及开发板电源。
⚫ 在 TCM 中执行程序
首先导入前面提到的 Helloworld 程序,然后将成功切换到“release”,编程成功后会在
【iar\release】目录下生成 hello_world.bin 镜像。
我们可以将该镜像复制到 SDcard 的根目录下,开发板通过读取 SDcard 目录下的镜像文
件加载到 TCM 中执行程序。
我们将开发板上电,使程序停留在 u-boot 控制终端。
通过 SDcard 加载镜像文件则执行如下指令
默认 TF 卡无分区
u-boot=> mmc dev 1
u-boot=> fatload mmc 1 0x48000000 hello_world.bin
u-boot=> cp.b 0x48000000 0x7e0000 0x20000
u-boot=> bootaux 0x7e0000
执行成功后在 M7 的串口终端上会看到有“hello world.”字符输出
Cortex-M7 协处理器自启动
上小节我们实现了 Cortex-M7 协处理器的手动启动,本小节主要实现 Cortex-M7 协处理
器的上电自启动。
Cortex-M7 协处理器与传统 M7 处理器不大相同,该处理器没有办法自己上电运行,需要
通过 u-boot 加载程序来引导 Cortex-M7 才行。
通过下面的指令可以将外部 SDcard 存储器的程序上电自动加载到 TCM 中执行,我们还
以上一章节的 Helloworld 程序镜像为例进行演示。
首先将 release 程序生成的镜像放到 SDcard 根目录,然后设置 m7_run 环境变量为“mmc
dev 1; fatload mmc 1:1 0x48000000 hello_world.bin; cp.b 0x48000000 0x7e0000 0x20000; bootaux 0x7e0000”这里的环境变量即为上小节提到的环境变量。
我们将开发板上电,使程序停留在 u-boot 控制终端。
之后输入如下指令设置环境变量
u-boot=> setenv m7_run 'mmc dev 1; fatload mmc 1:1 0x48000000 hello_world.bin; cp.b
0x48000000 0x7e0000 0x20000; bootaux 0x7e0000'
u-boot=> setenv boot_mmc 'mmc dev ${mmcdev}; if mmc rescan; then if run loadbootscript;
then run bootscript; else if run loadimage; then run mmcboot; else run netboot; fi; fi; fi;'
u-boot=> setenv bootcmd 'run m7_run; run boot_mmc'
u-boot=> saveenv
之后重启开发板即可看到在 M7 串口终端会自动运行程序。
本例中只是以 SDcard 下的 TCM 运行方式作为示例,按照此方法支持上小节提供的所有启
动方式,只需修改相应的 m7_run 环境变量即可。
LED 灯控制
硬件原理分析
只要是对硬件操作,就要首先查看原理图。查看外设是和 SoC 的哪个引脚相连。开发板
上的 LED 的亮灭状态,与芯片上的引脚 I/O 输出电平有关。
开发平台上 LED 的 I/O 扩展,在底板原理图中,查找 LED 原理图如下:
如图所示,可知控制 LED 灯的引脚编号为 GPIO_LED,然后继续查找这个编号。
在这个原理图下,可以查知其所接插座对应引脚的网络编号为 SD1_DATA5,然后在核心
板的原理图下查找这个网络编号。
如图所示,最终可以得到其所接芯片的引脚为 AA29,我们对这个引脚设置高低电平最终
便可控制 LED 灯的亮灭。
常用到的寄存器主要如下:
程序编写
首先介绍一下程序中用到的比较重要的结构体
⚫ gpio_pin_config_t :主要用于对 GPIO 引脚进行配置
typedef struct _gpio_pin_config
{
/*!< Specifies the pin direction. */
gpio_pin_direction_t direction;
/*!< Set a default output logic, which has no use in input */
uint8_t outputLogic;
/*!< Specifies the pin interrupt mode, a value of @ref gpio_interrupt_mode_t. */
gpio_interrupt_mode_t interruptMode;
} gpio_pin_config_t;
这里只提供部分重要代码,如想查看部分函数里的详细实现,请打开光盘资料对应的工
程【资料光盘\华清远见-I.MX8M Plus 开发资料\程序源码\Coertx-M7\igpio_led_output_task】。
#define EXAMPLE_LED_GPIO GPIO2
#define EXAMPLE_LED_GPIO_PIN 7U
volatile bool g_pinSet = false;
int main(void)
{
/* Define the init structure for the output LED pin*/
gpio_pin_config_t led_config = {kGPIO_DigitalOutput, 0, kGPIO_NoIntmode};
/* Board pin, clock, debug console init */
/* M7 has its local cache and enabled by default,
* need to set smart subsystems (0x28000000 ~ 0x3FFFFFFF)
* non-cacheable before accessing this address region */
BOARD_InitMemory();
/* Board specific RDC settings */
BOARD_RdcInit();
BOARD_InitBootPins();
BOARD_BootClockRUN();
BOARD_InitDebugConsole();
/* Print a note to terminal. */
/* Init output LED GPIO. */
GPIO_PinInit(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, &led_config);
while (1)
{
SDK_DelayAtLeastUs(100000, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
if (g_pinSet)
{
GPIO_PinWrite(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, 0U);
g_pinSet = false;
}
else
{
GPIO_PinWrite(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, 1U);
g_pinSet = true;
}
}
}
在此程序中,第 7 行定义了一个引脚配置的结构体, 然后从 13 行到 20 行是对开发板以
及系统的初始化,在第 25 行对 GPIO 进行初始化,第一个参数决定对哪路 GPIO 操作,第二个参数决定操作的哪个引脚,第三个参数是对引脚进行的配置。然后在 while 循环中,第 29行至 39 行实现控制 LED 灯的循环闪烁。
注意:程序运行参考《Cortex-M7 协处理器的启动》小节即可。
串口收发
硬件原理分析
串行通信的基础知识:
串口通信是指外设和计算机间,通过数据信号线、地线、控制线等,按位进行传输数据
的一种通讯方式。这种通信方式使用的数据线少,在远距离通信中可以节约通信成本,但其
传输速度比并行传输低。
串口是计算机上一种非常通用的设备通信协议。大多数计算机(不包括笔记本电脑)包
含两个基于 RS-232 的串口。串口同时也是仪器仪表设备通用的通信协议(串口通信协议也可以用于获取远程采集设备的数据)。
一条信息的各位数据被逐位按顺序传送的通讯方式称为串行通讯。
评价一个通讯是否优质,主要体现在传输的速度,数据的正确性,功耗是否低,布线成本是否低(例如 1 根线收发都能满足就比 8 根线的并行收发要节约成本);使用是否普及(就好像大家都学英语,世界很大部分的人都可以独立使用英语吗,会英语的人多,就非常普及,可通讯面就非常广;如果你学的鸟语,那就只能跟鸟通信,没有人能听懂)。串行通讯的特点是:数据位传送,传按位顺序进行,最少只需一根传输线即可完成,成本低但送速度慢。串行通讯的距离可以从几米到几千米。
RS-232(串口的英文代名词)采取不平衡传输方式,即所谓单端通讯,其共模抑制能力
差,其传送距离最长为约 15 米,最高速率为 20kb/s。RS-232 是为点对点(即只用一对收、
发设备)通讯而设计的。所以 RS-232 适合本地设备之间的通信。
传统的串行接口标准有 22 根线,采用标准 25 芯 D 型插头座(DB25),后来使用简化
为 9 芯 D 型插座(DB9),现在应用中 25 芯插头座已很少采用。
像现在所说的几线串口,一般都是指使用了几根线,最初的 RS-232 串口是 25 针的,所
有的针脚定义都有用到,后来变成了 9 针的,所谓全功能串口就是所有的针脚定义都使用上了,例如流量控制,握手信号等都有用到,一般来说国外的产品做产品比较规矩,把所有的串口信号都做上去了。但是国内的技术人员发现,其实 RS-232 串口最主要使用的就是 2,3 线,另外的接口如果不使用的话,也不会出现很大的问题,所以,就在 9 针的基础上做精简,所以就有所谓的 2,3,4,5,6,8 线的串口出来了。.
2 线串口只有 RXD,TXD 两根基本的收发信号线;3 线串口除了 RXD 和 TXD,还有
GND;所谓 4~9 线只是在 TXD 和 RXD 基础上增加了相应的控制信号线,依据实际需要进
行设计。一般来说,使用 5 线的 232 通信,是加了硬件流控的,即 RTS,CTS 信号,主要是为了保证高速通信时的可靠性,如果你的通信速度不是很高,完全可以不用理会。根据信息的传送方向,串行通讯可以进一步分为单工、半双工和全双工三种。信息只能单向传送为单工;信息能双向传送但不能同时双向传送称为半双工;信息能够同时双向传送则称为全双工。
•如果在通信过程的任意时刻,信息只能由一方 A 传到另一方 B,则称为单工。
•如果在任意时刻,信息既可由 A 传到 B,又能由 B 传 A,但只能由一个方向上的传输存
在,称为半双工传输。
•如果在任意时刻,线路上存在 A 到 B 和 B 到 A 的双向信号传输,则称为全双工。
并行通信和串行通信:
单片机与外界通信的基本方式有两种:并行通信和串行通信,串口属于串行通信。并行
通信是指利用多条数据传输线将一个数据的各位同时发送或接收。串行通信是指利用一条传
输线将数据一位位地顺序发送或接收。
并行通信和串行通信的示意图如下图:
在每一条传输线传输速率相同时,并行通信的传输速度比和串行通信快。然而当传输距
离变长时,并行通信的缺点就会凸显,首先是相比于串行通信而言信号易受外部干扰,信号
线之间的相互干扰也增加,其次是速率提升之后不能保证每根数据线的数据同时到达接收方
而产生接收错误,而且距离越长布线成本越高。
所以并行通信目前主要用在短距离通信,比如处理器与外部的 flash 以及外部 RAM 以
及芯片内部各个功能模块之间的通信。串行通信以其通信速率快和成本低等优点成为了远距
离通信的首选。RS232 串口,以及差分串行总线像 RS485 串口、USB 接口、CAN 接口、
IEEE-1394 接口、以太网接口、SATA 接口和 PCIE 接口等都属于串行通信的范畴。
下图左侧为每根数据线的数据同时到达接收方,被正确采样的最理想情况;右侧的图为
每根数据线的数据不能同时到达接收方而产生接收错误情形。
串口的原理和特点:
串口是计算机上一种非常通用设备通信的协议(不要与通用串行总线 Universal Serial Bus
或者 USB 混淆)。大多数计算机包含两个基于 RS232 的串口。串口同时也是仪器仪表设备
通用的通信协议;很多 GPIB(通用接口总线)兼容的设备也带有 RS-232 口。同时,串口通信
协议也可以用于获取远程采集设备的数据。
串口通信的概念非常简单,串口按位(bit)发送和接收字节。尽管比按字节(byte)的并
行通信慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。它很简单并且
能够实现远距离通信。典型地,串口用于 ASCII 码字符的传输。通信使用 3 根线完成:(1)
地线,(2)发送,(3)接收。由于串口通信是异步的,端口能够在一根线上发送数据同时在
另一根线上接收数据。其他线用于握手,但是不是必须的。串口通信最重要的参数是波特率、
数据位、停止位和奇偶校验。
对于两个进行通行的端口,这些参数必须匹配:
波特率:
这是一个衡量通信速度的参数。它表示每秒钟传送的 bit 的个数。例如 300 波特
表示每秒钟发送 300 个 bit。当我们提到时钟周期时,我们就是指波特率例如如果协议需要4800 波特率,那么时钟是 4800Hz。这意味着串口通信在数据线上的采样率为 4800Hz。通常电话线的波特率为 14400,28800 和 36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是 GPIB 设备的通信。
数据位:
这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据不
会是 8 位的,标准的值是 5、7 和 8 位。如何设置取决于你想传送的信息。比如,标准的 ASCII码是 0~127(7 位)。扩展的 ASCII 码是 0~255(8 位)。如果数据使用简单的文本(标准ASCII 码),那么每个数据包使用 7 位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。
停止位:
用于表示单个包的最后一位。典型的值为 1 ,1.5 和 2 位。这里的 1.5 位的数据宽度,就是 1.5 个波特率,由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
奇偶校验位:
在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。
当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一
位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是 011,那么对于偶校验,校验位为 0,保证逻辑高的位数是偶数个。如果是奇校验,校验位位 1 ,这样就有3 个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步.
硬件流控制:
硬件流控制常用的有 RTS/CTS 流控制和 DTR/ R(数据终端就绪/数据设置
就绪)流控制。硬件流控制必须将相应的电缆线连上,用 RTS/CTS(请求发送/清除发送)流控制时,应将通讯两端的 RTS、CTS 线对应相连,数据终端设备(如计算机)使用 RTS 来起始调制解调器或其它数据通讯设备的数据流,而数据通讯设备(如调制解调器)则用 CTS 来起动和暂停来自计算机的数据流。这种硬件握手方式的过程为:我们在编程时根据接收端缓冲区大小设置一个高位标志(可为缓冲区大小的 75%)和一个低位标志(可为缓冲区大小的25%),当缓冲区内数据量达到高位时,我们在接收端将 CTS 线置低电平(送逻辑 0),当发送端的程序检测到 CTS 为低后,就停止发送数据,直到接收端缓冲区的数据量低于低位而将 CTS 置高电平。RTS 则用来标明接收设备有没有准备好接收数据。
常用的流控制还有还有 DTR/ R(数据终端就绪/数据设置就绪)。我们在此不再详述。
打开我们的底板原理图,查找 M7 用于串行通信的串口,如图所示:
如图所示,可知 M7 核使用的串口为 UART4,然后继续查找这个编号。
在这个原理图下,可以查知其所接插座对应引脚的网络编号为 UART4_TXD 与
UART4_RXD,然后在核心板的原理图下查找此网络编号
如图所示,便可以得到所用串口的 RXD 与 TXD 连接芯片的引脚为 AJ5 与 AH3。
⚫ 连接调试串口
按照上图圈出的 M7 调试串口(下向上分别为 5V、M4_TX、M4_RX、A53_TX、A53_RX、
GND)。这里我们只连接 GND、M7_RX、M7_TX 三条线。
主要控制器配置
主要用到的寄存器如下,因为并不是每个寄存器的所有位都会用到,这里只是介绍每个
寄存器中所用到的某几位。
程序编写
首先介绍一下程序中用到的比较重要的结构体。
⚫ struct _uart_config:此结构体用于对 UART 串口进行配置
typedef struct _uart_config
{
/*!< UART baud rate. */
uint32_t baudRate_Bps;
/*!< Parity error check mode of this module. */
uart_parity_mode_t parityMode;
/*!< Data bits count, eight (default), seven */
uart_data_bits_t dataBitsCount;
/*!< Number of stop bits in one frame. */
uart_stop_bit_count_t stopBitCount;
/*!< TX FIFO watermark */
uint8_t txFifoWatermark;
、
/*!< RX FIFO watermark */
uint8_t rxFifoWatermark;
/*!< RX RTS watermark, RX FIFO data count being larger than \
this triggers RTS deassertion */
uint8_t rxRTSWatermark;
/*!< Enable automatic baud rate detection */
bool enableAutoBaudRate;
/*!< Enable TX */
bool enableTx;
/*!< Enable RX */
bool enableRx;
/*!< RX RTS enable */
bool enableRxRTS;
/*!< TX CTS enable */
bool enableTxC
其成员变量各作用如下
struct _uart_transfer:UART 传输结构体,一般传输数据时会用到此结构体
typedef struct _uart_transfer
{
union
{
uint8_t *data; /*!< The buffer of data to be transfer.*/
uint8_t *rxData; /*!< The buffer to receive data. */
const uint8_t *txData; /*!< The buffer of data to be sent. */
};
size_t dataSize; /*!< The byte count to be transfer. */
} uart_transfer_t;
⚫ struct _uart_transfer:UART 传输结构体,一般传输数据时会用到此结构体
typedef struct _uart_transfer
{
union
{
uint8_t *data; /*!< The buffer of data to be transfer.*/
uint8_t *rxData; /*!< The buffer to receive data. */
const uint8_t *txData; /*!< The buffer of data to be sent. */
};
size_t dataSize; /*!< The byte count to be transfer. */
} uart_transfer_t;
然后介绍一下用到的函数。
⚫ status_t UART_Init 函数
⚫ status_t UART_TransferSendNonBlocking 函数
⚫ status_t UART_TransferReceiveNonBlocking 函数
⚫ void UART_TransferCreateHandle 函数
这里只提供部分重要代码,如想查看部分函数里的详细实现,请打开光盘资料对应的工
程【资料光盘\华清远见-I.MX8M Plus 开发资料\程序源码\CoertxM7\iuart_interrupt_transfer_task】。
int main(void)
{
status_t status;
uart_config_t config;
uart_transfer_t xfer;
uart_transfer_t sendXfer;
uart_transfer_t receiveXfer;
/* M7 has its local cache and enabled by default,
* need to set smart subsystems (0x28000000 ~ 0x3FFFFFFF)
* non-cacheable before accessing this address region */
BOARD_InitMemory();
/* Board specific RDC settings */
BOARD_RdcInit();
BOARD_InitBootPins();
BOARD_BootClockRUN();
/*
* config.baudRate_Bps = 115200U;
* config.parityMode = kUART_ParityDisabled;
* config.dataBitsCount = kUART_EightDataBits;
* config.stopBitCount = kUART_OneStopBit;
* config.txFifoWatermark = 2;
* config.rxFifoWatermark = 16;
* config.enableTx = false;
* config.enableRx = false;
*/
UART_GetDefaultConfig(&config);
config.baudRate_Bps = BOARD_DEBUG_UART_BAUDRATE;
config.rxFifoWatermark = 16;
config.enableTx = true;
config.enableRx = true;
status = UART_Init(DEMO_UART, &config, DEMO_UART_CLK_FREQ);
if (kStatus_Success != status)
{
return kStatus_Fail;
}
UART_TransferCreateHandle(DEMO_UART, &g_uartHandle, UART_UserCallback, NULL);
/* Start to echo. */
sendXfer.data = g_txBuffer;
sendXfer.dataSize = ECHO_BUFFER_LENGTH;
receiveXfer.data = g_rxBuffer;
receiveXfer.dataSize = ECHO_BUFFER_LENGTH;
while (1)
{
/* If RX is idle and g_rxBuffer is empty, start to read data to g_rxBuffer. */
if ((!rxOnGoing) && rxBufferEmpty)
{
rxOnGoing = true;
UART_TransferReceiveNonBlocking(DEMO_UART, &g_uartHandle, &receiveXfer, NULL);
}
/* If TX is idle and g_txBuffer is full, start to send data. */
if ((!txOnGoing) && txBufferFull)
{
txOnGoing = true;
UART_TransferSendNonBlocking(DEMO_UART, &g_uartHandle, &sendXfer);
}
/* If g_txBuffer is empty and g_rxBuffer is full, copy g_rxBuffer to g_txBuffer. */
if ((!rxBufferEmpty) && (!txBufferFull))
{
memcpy(g_txBuffer, g_rxBuffer, ECHO_BUFFER_LENGTH);
rxBufferEmpty = true;
txBufferFull = true;
}
}
}
在此程序中,第 12 行至 18 行完成对开发板以及内核的初始化,在第 29 行得到串口的默
认配置,然后在 30 行至 33 行,对串口进行修改配置,包括设置串口波特率和接收 FIFO 的大小、使能 TX 与 RX。完成以后在 35 行对串口进行初始化。在 41 行初始化 UART 句柄,在此用户需要自定义一个回调函数 UART_UserCallback,此回调函数用于判断状态标志。
在 while 中,54 行判断是否在接收数据以及接收缓冲区是否为空,满足条件时开始接收
数据,将接收到的数据放到 g_rxBuffer 中。在 68 行判断发送缓冲区是否为空以及接收缓冲区是否为满,符合条件时将接收缓冲区的内容复制到发送缓冲区中。61 行判断是否正在进行发送数据以及发送缓冲区是否为满,当满足条件时将 g_txBuffer 中的数据发送出去。最终实现字符串的接收与发送。另外,在本程序中,缓冲区大小 ECHO_BUFFER_LENGTH 定义为 8,因此每次发送、接收都是八个字符。程序运行参考《Cortex-M7 协处理器的启动》小节即可。实验结果如下:
FreeRTOS 实时操作系统应用
FreeRTOS 基本接口介绍
RTOS 全称为 Real Time Operation System,即实时操作系统。RTOS 强调的是实时性,又
分为硬实时和软实时。硬实时要求在规定的时间内必须完成操作,不允许超时;而软实时里
对处理过程超时的要求则没有很严格。RTOS 的核心就是任务调度
FreeRTOS 是 RTOS 的一种,尺寸非常小,可运行于微控制器上。微控制器是尺寸小,资
源受限的处理器,它在单个芯片上包含了处理器本身、用于保存要执行的程序的只读存储器
(ROM 或 Flash)、所执行程序需要的随机存取存储器(RAM),一般情况下程序直接从只
读存储器执行FreeRTOS 作为一种嵌入式系统使用的开源实时操作系统,也可以支持许多不同硬件架构以及交叉编译器。
下面介绍一下 FreeRTOS 操作系统常用到的功能函数,主要如下:
⚫ xTaskCreate 函数
⚫ xTaskCreate 函数
⚫ UART_RTOS_Receive 函数
⚫ UART_RTOS_Send 函数
⚫ xSemaphoreCreateBinary(void) 函数
⚫ xSemaphoreGive(xSemaphore) 函数
⚫ SemaphoreTake(SemaphoreHandle_t xSemaphore,TickType_t xBlockTime) 函数
基于 FreeRTOS 的按键点灯控制系统
本次实验是通过一个按键来控制 LED 灯的点亮与熄灭,整个程序的控制流程图如下所示:
主要代码如下,这里只提供部分重要代码,如想查看部分函数里的详细实现,请打开光
盘 资 料对 应的 工程【 资 料光 盘\华 清远见 -I.MX8M Plus 开发 资 料\程 序源 码 \CoertxM7\freertos_sem_test】。
int main(void)
{
gpio_pin_config_t led_config = {kGPIO_DigitalOutput, 0, kGPIO_NoIntmode};
GPIO_PinInit(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, &led_config);
xSemaphore_producer = xSemaphoreCreateBinary();
if (xSemaphore_producer == NULL)
{
PRINTF("xSemaphore_producer creation failed.\r\n");
vTaskSuspend(NULL);
}
xSemaphore_consumer = xSemaphoreCreateBinary();
if (xSemaphore_consumer == NULL)
{
PRINTF("xSemaphore_consumer creation failed.\r\n");
vTaskSuspend(NULL);
}
if (xTaskCreate(producer_task, "PRODUCER_TASK", configMINIMAL_STACK_SIZE + 128, \
NULL, TASK_PRIO, NULL) != pdPASS)
{
- 171 -
PRINTF("Task creation failed!.\r\n");
while (1)
;
}
if (xTaskCreate(consumer_task, "CONSUMER_TASK", configMINIMAL_STACK_SIZE + 128, \
NULL, TASK_PRIO, NULL) != pdPASS)
{
PRINTF("Task creation failed!.\r\n");
vTaskSuspend(NULL);
}
/* Start scheduling. */
vTaskStartScheduler();
for (;;)
;
}
static void producer_task(void *pvParameters)
{
PRINTF("Producer_task created.\r\n");
while (1)
{
xSemaphoreGive(xSemaphore_consumer);
while(GPIO_PinRead(EXAMPLE_KEY_GPIO, EXAMPLE_KEY_GPIO_PIN) == 1);
if (xSemaphoreTake(xSemaphore_producer, portMAX_DELAY) == pdTRUE)
{
GPIO_PinWrite(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, 1U);
PRINTF("LED OFF \n");
SDK_DelayAtLeastUs(100000, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
}
}
}
static void consumer_task(void *pvParameters)
{
while (1)
{
if (xSemaphoreTake(xSemaphore_consumer, portMAX_DELAY) == pdTRUE)
{
GPIO_PinWrite(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, 0U);
PRINTF("LED ON \n");
SDK_DelayAtLeastUs(100000, SDK_DEVICE_MAXIMUM_CPU_CLOCK_FREQUENCY);
}
xSemaphoreGive(xSemaphore_producer);
在主函数中,第 4 行与 11 行定义了两个信号量,在 18 行和 25 行创建了两个任务,然后在 33 行开始运行任务。
在 producer_task 任务中,43 行阻塞等待按键是否按下,当满足条件时,45 行释放
xSemaphore_consumer 信号量。在 consumer_task 任务中,获取到 xSemaphore_consumer 信号量以后,执行点灯操作,然后再释放 xSemaphore_producer 信号量,此时 producer_task 任务获取到这个信号量以后,再执行灭灯操作。
程序运行参考《Cortex-M7 协处理器的启动》小节即可。
Cortex-M7 端多核通讯的实现
此开发板支持 Cortex-M7 与 Cortex-A53 两个核心,所谓多核通信即指 Cortex-M7 核与
Cortex-A53 核之间进行数据交互。
在实现多核通信时,需要用到一些协议库,主要包括:
(1) 嵌入式远程过程调用(eRPC):该组件是库和代码生成器工具的组合,实现了对远程
服务(运行在不同的核心上)的透明函数调用接口。
(2) 多核管理器(MCMGR):这个库维护所有核的信息,并启动辅助核。
其主要功能主要如下:
⚫ 维护系统中所有核心的信息
⚫ 二次/辅助核心启动和关闭
⚫ 远程核心监控和事件处理
(3) (RPMsg-Lite):处理器间通信库。
这里需要提到 RPMsg 协议,RPMSG 全称 Remote Processor Messaging,其定义了一个标
准的二进制接口,用于异构多核系统中多个核之间的通信。RPMsg 作为处理器间的消息传输总线,其中每个处理器都是总线上的器件。它允许内核驱动程序与系统上的远程处理器进行通信,同时,驱动程序可以根据需要公开适当的用户空间接口。与遗留的 OpenAMP 实现相比,RPMsg-Lite 提供了代码大小的缩减、API 的简化和改进的模块化。
RPMsg 协议的主要特点:
⚫ 共享内存处理器间通信
⚫ 基于 virtio 的消息传递总线
⚫ 端点之间发送的应用程序定义的消息
⚫ 可移植到不同的环境/平台
⚫ 在 upstream Linux 操作系统可用
基于此,本次实验主要实现 Cortex-M7 与 Cortex-A53 之间的数据传输。
Cortex-M7 端程序实现
首先介绍一下 Cortex-M7 部分主要使用的库函数:
⚫ *rpmsg_lite_remote_init 函数
M7 端程序主要如下,这里只提供部分重要代码,如想查看部分函数里的详细实现,请打
开光盘资料对应的工程【资料光盘\华清远见-I.MX8M Plus 开发资料\程序源码\CoertxM7\multicore-communication\rpmsg_lite_str_echo_rtos_imxcm7_test】。
int main(void)
{
BOARD_InitMemory();
/* Board specific RDC settings */
BOARD_RdcInit();
BOARD_InitBootPins();
BOARD_BootClockRUN();
BOARD_InitDebugConsole();
copyResourceTable();
#ifdef MCMGR_USED
/* Initialize MCMGR before calling its API */
(void)MCMGR_Init();
#endif /* MCMGR_USED */
gpio_pin_config_t led_config = {kGPIO_DigitalOutput, 0, kGPIO_NoIntmode};
GPIO_PinInit(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, &led_config);
if (xTaskCreate(app_task, "APP_TASK", APP_TASK_STACK_SIZE, NULL, \
tskIDLE_PRIORITY + 1, &app_task_handle) != pdPASS)
{
PRINTF("\r\nFailed to create application task\r\n");
for (;;)
;
}
vTaskStartScheduler();
PRINTF("Failed to start FreeRTOS on core0.\n");
for (;;)
;
}
static void app_task(void *param)
{
volatile uint32_t remote_addr;
struct rpmsg_lite_endpoint *volatile my_ept;
volatile rpmsg_queue_handle my_queue;
struct rpmsg_lite_instance *volatile my_rpmsg;
void *rx_buf;
uint32_t len;
int32_t result;
void *tx_buf;
uint32_t size;
/* Print the initial banner */
PRINTF("\r\nRPMSG String Echo FreeRTOS RTOS API Demo...\r\n");
my_rpmsg = rpmsg_lite_remote_init((void *)RPMSG_LITE_SHMEM_BASE, \
RPMSG_LITE_LINK_ID, RL_NO_FLAGS);
while (0 == rpmsg_lite_is_link_up(my_rpmsg))
;
my_queue = rpmsg_queue_create(my_rpmsg);
my_ept = rpmsg_lite_create_ept(my_rpmsg, LOCAL_EPT_ADDR, rpmsg_queue_rx_cb, my_queue);
(void)rpmsg_ns_announce(my_rpmsg, my_ept, RPMSG_LITE_NS_ANNOUNCE_STRING, RL_NS_CREATE);
PRINTF("\r\nNameservice sent, ready for incoming messages...\r\n");
for (;;)
{
/* Get RPMsg rx buffer with message */
result = rpmsg_queue_recv_nocopy(my_rpmsg, my_queue, (uint32_t *)&remote_addr, \
(char **)&rx_buf, &len, RL_BLOCK);
if (result != 0)
{
assert(false);
}
/* Copy string from RPMsg rx buffer */
assert(len < sizeof(app_buf));
memcpy(app_buf, rx_buf, len);
app_buf[len] = 0; /* End string by '\0' */
if ((len == 2) && (app_buf[0] == 0xd) && (app_buf[1] == 0xa))
PRINTF("Get New Line From Master Side\r\n");
else
PRINTF("Get Message From Master Side : \"%s\" [len : %d]\r\n", app_buf, len);
/* Get tx buffer from RPMsg */
tx_buf = rpmsg_lite_alloc_tx_buffer(my_rpmsg, &size, RL_BLOCK);
assert(tx_buf);
if(strncmp("LED ON", app_buf, len) == 0)
{
GPIO_PinWrite(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, 1U);
/* Copy string to RPMsg tx buffer */
memcpy(tx_buf, "led on", len);
}
else if(strncmp("LED OFF", app_buf, len) == 0)
{
GPIO_PinWrite(EXAMPLE_LED_GPIO, EXAMPLE_LED_GPIO_PIN, 0U);
/* Copy string to RPMsg tx buffer */
memcpy(tx_buf, "led off", len);
}
/* Echo back received message with nocopy send */
result = rpmsg_lite_send_nocopy(my_rpmsg, my_ept, remote_addr, \
tx_buf, len);
if (result != 0)
{
assert(false);
}
/* Release held RPMsg rx buffer */
result = rpmsg_queue_nocopy_free(my_rpmsg, rx_buf);
if (result != 0)
{
assert(false);
}
}
}
在本次 A53 与 M7 的多核通信中,主要实现的效果如下:A53 核向 M7 循环发送“LED
ON”或“LED OFF”的指令,M7 接收指令以后,分别控制 LED 灯的亮灭,同时再向 A53 发
送“led on”或“led off”的字符串。
首先在主函数中,第 6 行至 12 行完成对开发板以及内核的初始化,然后在第 19 行通过
FreeRTOS 创建了一个任务。之后运行到这个任务中,先定义一些程序中所需要的变量,在 50
行初始化堆栈,会得到一个实例指针,52 行阻塞等待 M7 与 A53 是否已经链接,当链接成功以后,继续往下运行,然后再 55、56 行创建所需要的队列和 RPMSG 端点,这些用于数据发送与接收。
进入到 for 循环以后,在 64 行阻塞接收数据,85 行与 92 行判断是否为控制 LED 灯指令,进而执行亮灯或灭灯操作,然后在 100 行回复相对应的字符串。
程序运行参考《Cortex-M7 协处理器的启动》小节即可。
Cortex-A53 端程序实现
A53 端程序主要如下,这里只提供部分重要代码,如想查看部分函数里的详细实现,请
打开光盘资料对应的工程【资料光盘\华清远见-I.MX8M Plus 开发资料\程序源码\CoertxM7\multicore-communication\multicore-A53】。
Multicore-a53.c
int main(int argc, char *argv[])
{
int i = 0, count = 0;
char tem1[32] = {0};
struct termios opt;
char buf[512] = {0};
int fd;
// strcpy(tem1, "/dev/ttyRPMSG30");
if((fd = open(argv[1], O_RDWR | O_NOCTTY )) < 0)
{
printf("open is error.\n");
return -1;
}
printf("open is ok!\n");
printf("uart init start.\n");
tcgetattr(fd, &opt);
cfsetispeed(&opt, B115200);
cfsetospeed(&opt, B115200);
//tcflush(fd, TCIOFLUSH);
opt.c_cflag |= (CLOCAL | CREAD);
opt.c_cflag &= ~CSIZE;
opt.c_cflag &= ~CRTSCTS;
opt.c_cflag |= CS8;
opt.c_iflag |= IGNPAR;
opt.c_cflag &= ~CSTOPB;
opt.c_oflag = 0;
opt.c_lflag = 0;
tcsetattr(fd, TCSANOW, &opt);
printf("uart init end.\n");
while(1)
{
count = write(fd, "LED ON", 6);
sleep(1);
count = read(fd, buf, sizeof(buf));
printf("read buf count: %d\n", count);
printf("read buf : %s\n", buf);
memset(buf, 0, sizeof(buf));
sleep(1);
count = write(fd, "LED OFF", 7);
count = read(fd, buf, sizeof(buf));
printf("read buf count: %d\n", count);
printf("read buf : %s\n", buf);
memset(buf, 0, sizeof(buf));
sleep(1);
}
return 0;
}
在 A53 程序中,第 3 行至第 7 行定义了程序中需要的变量,其中第 5 行定义的结构体用于提供健全的线路设置集合,主要配置 M7 与 A53 双核通信的串口通道。在 11 行打开我们所使用的通道,第 20 行至 37 行完成对结构体成员的配置。
在 while 循环中,调用 write 函数分别发送“LED ON”或者“LED OFF”的字符串,每
次发送完都使用 read 接收 M7 核返回的数据,进行循环运行。
对于 A53 部分,程序编写完成以后,下一步需要进行编译。编译之前,需要先安装 imxrobot-xwayland 交叉编译工具链,安装过程可参考《交叉编译工具链安装》小节。
⚫ 导入 SDK
linux@ubuntu: $ source /opt/fsl-imx-xwayland/5.4-zeus/environment-setup-aarch64-poky-li
Nux
验证开发工具是否安装正确,显示版本信息如下图所示。
linux@ubuntu: $ $CC --version
⚫ 编译并执行
编译前请确保 SDK 已经导入,前面我们已经导入了 SDK。另外还需要添加一个 Makefile
文件,我们在源码路径下提供了编写好的 Makefile 文件,可以直接使用,执行 make 编译即可生成 test 可以执行程序。
linux@ubuntu: $ make
运行程序
将上一步生成的 test 可执行程序,导入到目标板。这里使用 scp 的方式。
linux@ubuntu: $ scp test root@192.168.100.100:~
这里 root 为目标板的用户名,192.168.100.100 为目标板的 IP 地址,~为目标板的家目录。
程序执行步骤
程序编写完成以后,打开用于连接 A53 核的串口,启动到 u-boot 状态,另外再打开连接M7 核的串口,然后启动 Cortex-M7 协处理器,显示如下:
另外对于 A53 核,在 u-boot 状态,我们需要设置一下环境变量
u-boot=> setenv fdt_file imx8mp-ai-robot-rpmsg.dtb
u-boot=> save
Saving Environment to MMC... Writing to MMC(1)... OK
然后可以执行 boot 启动到内核
u-boot=> boot
进入到内核之前,需要输入登录密码,密码为 root
此时在 M7 核程序中已经判断连接成功,便会再打印一条语句,如下所示:
A53 进入到内核以后,首先需要加载一个驱动文件,这个文件用于产生 M7 与 A53 双核通信的通道,同时在执行 A53 核程序以后,还会向 M7 发送“hello world!”字符串。
root@imx-robot-kinetic:~# modprobe imx_rpmsg_tty
此时查看 dev 目录下 tty 设备,可以看到多出了一个 RPMSG 的串口,这个便就是用于
A53 与 M7 双核通信。
将 A53 核程序生成的可执行文件发送到开发板中,在开发板终端下进行执行。
root@imx-robot-kinetic:~# ./test /dev/ttyRPMSG30
运行结果分别如下