IMX6ULL嵌入式AI模型部署项目。实现模拟车载氛围灯。使用STM32MP157-M4(可替换为其它低端MCU)充当氛围灯控制器,外接LED灯带,使用IMX6ULL充当网关部署Tensorflow Lite分类模型。利用STM32开发板自带ICM_20608和AP3216C采集位姿与环境光数据,将采集数据通过CAN发送至网关,输入神经网络模型,运行推断并返回分类结果,实现模拟汽车不同环境行驶状态下的氛围灯效果。
一. 整体架构流程
1. 功能流程
STM32开发板使用FreeRTOS系统,利用板载传感器采集环境光和六轴传感器数据,进行数据预处理,然后通过CAN总线发送至IMX6ULL。IMX6ULL在接收到CAN报文后解析报文并将数据输入至Tensorflow Lite解释器中,运行模型推断,产生分类结果,通过CAN发送回STM32开发板上,STM32开发板在收到分类结果后利用PWM运行不同的灯带控制逻辑。
最终实现在STM32开发板模拟汽车在日间上坡、下坡、静止,夜间上坡、下坡、静止六种不同状态下的氛围灯不同效果,实现AI自适应环境目的。
2. 实现流程
(1)制作数据集:编写STM32开发板传感器数据采集程序,利用开发板本身模拟汽车在日间上坡、下坡、静止,夜间上坡、下坡、静止六种不同状态。通过串口保存各个状态下的数据,并保存至.csv文件中,制作数据集。
(2)生成神经网络模型:使用Tensorflow框架构建神经网络分类模型,在这个步骤我添加了几个简单的全连接层,在最后使用Softmax函数进行分类,若对此方面有要求可进行优化,本文仅涉及部署与实验流程。
(3)转换为.tflite模型:利用tf.lite.TFLiteConverter将训练好的模型转换为Tensorflow Lite模型。
(4)编写应用层代码:应用层代码涉及IMX6ULL CAN通信以及Tensorflow Lite模型解释与推断。
(5)生成可执行文件:修改CMakeLists脚本,利用CMake工具最终编译生成可执行文件。
(6)固化功能至Linux系统:将功能添加至IMX6ULL系统进程中去,使能相关驱动,固化文件系统。
(7)编写STM32开发板程序:编写具有CAN收发,PWM控制,传感器数据采集的FreeRTOS程序,并下载到开发板中。
(8)连接硬件并调试
二. 准备工作
1. 硬件准备
(1)IMX6ULL开发板(我使用的正点原子IMX6ULL MINI开发板)要求具有CAN通信功能。
(2)STM32开发板(我使用的百问网STM32MP157开发板)要求具有环境光和位姿传感器,具有CAN通信接口和PWM输出接口。不需要MP157这么高的配置,用一块简单的MCU开发板也可进行替代,实现实验效果。
(3)RGBLED灯带(我使用的WS2812B灯带),要求不限,可根据喜好选择,甚至可以选择更加接近车载氛围灯的灯带。
(4)杜邦线:用于连接灯带,连接CAN实现通信。
2. 软件环境
(1)Ubuntu18.04负责编写IMX6ULL应用层代码,要求安装CMake工具,下载Tensorflow Lite源码和其它必要环境。
(2)Windows负责编写M4核程序,要准备好Keil或其他相关工具链。要求准备好Tensorflow的运行环境用于构建神经网络模型。
三. 关键步骤与源码解析
1. 生成并转换Tensorflow模型
首先可以利用开发板的官方教程自行进行修改使串口打印传感器数据,用逗号隔开。这一步我采集了AP3216C的ALS值和IR值以及ICM20608G的温度值和六轴传感器值,并将六轴传感器值转换成了位姿角,保留了pitch和roll角。为增加复杂度,将输入按位姿前后采样点进行输入值的复杂化。最终数据集效果如图,最后一列为人工标注的分类标号,我使用的1-6
使用python进行数据集读取和网络的构建。部分代码如下:
#将actual列(分类)设为targets targets = np.array(features['actual']) targets -= 1 labels = tf.keras.utils.to_categorical(targets, num_classes=6) # 获取特征值x,即在原数据中去掉目标值列,默认删除行,需要指定轴axis=1指向列 features = features.drop('actual',axis=1) # 把features从DateFrame变成数组 features = np.array(features)
#建立简单全连接网络分类模型(模型根据自己需要进行调整,这里仅供简单实验) model = tf.keras.Sequential([ tf.keras.layers.Dense(4, activation='relu', input_shape=(8,)), tf.keras.layers.Dense(16, activation='relu'), tf.keras.layers.Dense(6, activation='softmax') ]) # 编译模型 model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy']) model.fit(train_features, train_labels, epochs=5, batch_size=16, verbose=1)
转换Tensorflow模型为Tensorflow Lite模型:
converter = tf.lite.TFLiteConverter.from_keras_model(model) tflite_model = converter.convert() with open('model2.tflite', 'wb') as f: f.write(tflite_model)
2. 搭建Tensorflow Lite框架
IMX6ULL作为ARM32位开发板,应使用CMake方式构建Tensorflow Lite框架。
Build TensorFlow Lite with CMake (google.cn)
(1)下载CMake(3.16以上)以及Tensorflow框架源码
sudo apt-get install cmake git clone https://github.com/tensorflow/tensorflow.git tensorflow_src
(2)新建路径并在路径下编译Tensorflow Lite源码
mkdir tflite_build cd tflite_build cmake ../tensorflow_src/tensorflow/lite
在编译过程中我出现了如下错误:
当出现这样的错误时,仅需在CMakeLists中取消xnnpack相关编译即可。
此时就完成了框架环境的编译,可以进行应用层代码的编写了。
3. 应用层代码编写与编译
应用层的实验代码根据官方minimal模板下进行修改。此部分主要做的是CAN的通信以及模型文件的读取,输入模型,运行推断,以及输出结果。
int main(int argc, char* argv[]) { float pitch_1 = 1, roll_1 = 1, temp_1 = 1, als = 1, ir = 1, pitch_2 = 1, roll_2 = 1, temp_2 = 1; u_int8_t mode; if (argc != 2) { fprintf(stderr, "minimal <tflite model> "); return 1; } CAN_Config(); const char* filename = argv[1]; // Load model std::unique_ptr<tflite::FlatBufferModel> model = tflite::FlatBufferModel::BuildFromFile(filename); TFLITE_MINIMAL_CHECK(model != nullptr); while(1){ canReceive(); //first frame of a message (receiveframe.data[0] == 0x01) ? (pitch_1 = pitch_1) : (pitch_1 = -pitch_1); pitch_1 *= receiveframe.data[1] * 10000; pitch_1 += receiveframe.data[2] * 100; pitch_1 += receiveframe.data[3]; (receiveframe.data[4] == 0x01) ? (roll_1 = roll_1) : (roll_1 = -roll_1); roll_1 *= receiveframe.data[5] * 10000; roll_1 += receiveframe.data[6] * 100; roll_1 += receiveframe.data[7]; canReceive(); //second frame of a message (receiveframe.data[0] == 0x01) ? (temp_1 = temp_1) : (temp_1 = -temp_1); temp_1 *= receiveframe.data[1] * 10000; temp_1 += receiveframe.data[2] * 100; temp_1 += receiveframe.data[3]; //(receiveframe.data[4] == 0x01) ? () : (als = -als); als *= receiveframe.data[5] * 10000; als += receiveframe.data[6] * 100; als += receiveframe.data[7]; canReceive(); //third frame of a message (receiveframe.data[0] == 0x01) ? (ir = ir) : (ir = -ir); ir *= receiveframe.data[1] * 10000; ir += receiveframe.data[2] * 100; ir += receiveframe.data[3]; pitch_1 /= 10000; roll_1 /= 10000; temp_1 /= 10000; ir /= 10000; // Build the interpreter with the InterpreterBuilder. // Note: all Interpreters should be built with the InterpreterBuilder, // which allocates memory for the Interpreter and does various set up // tasks so that the Interpreter can read the provided model. tflite::ops::builtin::BuiltinOpResolver resolver; tflite::InterpreterBuilder builder(*model, resolver); std::unique_ptr<tflite::Interpreter> interpreter; builder(&interpreter); TFLITE_MINIMAL_CHECK(interpreter != nullptr); // Allocate tensor buffers. TFLITE_MINIMAL_CHECK(interpreter->AllocateTensors() == kTfLiteOk); //printf("=== Pre-invoke Interpreter State === "); //tflite::PrintInterpreterState(interpreter.get()); //edited by fuan ning int input_index = interpreter->inputs()[0]; float* input_data = interpreter->typed_tensor<float>(input_index); input_data[0] = pitch_1; input_data[1] = roll_1; input_data[2] = temp_1; input_data[3] = als; input_data[4] = ir; input_data[5] = pitch_2; input_data[6] = roll_2; input_data[7] = temp_2; pitch_2 = pitch_1; roll_2 = roll_1; temp_2 = temp_1; for(int ii = 0; ii < 8; ii++) { printf("inputdata[%d] is %.4f ", ii, input_data[ii]); } // Fill input buffers // TODO(user): Insert code to fill input tensors. // Note: The buffer of the input tensor with index `i` of type T can // be accessed with `T* input = interpreter->typed_input_tensor<T>(i);` // Run inference TFLITE_MINIMAL_CHECK(interpreter->Invoke() == kTfLiteOk); //printf(" === Post-invoke Interpreter State === "); //tflite::PrintInterpreterState(interpreter.get()); //edited by fuan ning int output_index = interpreter->outputs()[0]; float* output_data = interpreter->typed_tensor<float>(output_index); printf("output mode 1 possibility is %f ", output_data[0]); printf("output mode 2 possibility is %f ", output_data[1]); printf("output mode 3 possibility is %f ", output_data[2]); printf("output mode 4 possibility is %f ", output_data[3]); printf("output mode 5 possibility is %f ", output_data[4]); printf("output mode 6 possibility is %f ", output_data[5]); double max = 0; for(int ii = 0; ii < 6; ii++) { if(output_data[ii] > max) { max = output_data[ii]; mode = ii; } } printf("mode is %d ", mode + 1); pitch_1 = 1, roll_1 = 1, temp_1 = 1, als = 1, ir = 1; //sleep(3); sendframe.data[0] = mode + 1; canSend(); } return 0; }
指定工具链与编译选项,进行应用层代码可执行文件的编译。
ARMCC_FLAGS="-march=armv7-a -mfpu=neon-vfpv4 -funsafe-math-optimizations -mfp16-format=ieee" ARMCC_PREFIX=/home/book/toolchains/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf- cmake -DCMAKE_C_COMPILER=${ARMCC_PREFIX}gcc -DCMAKE_CXX_COMPILER=${ARMCC_PREFIX}g++ -DCMAKE_C_FLAGS="${ARMCC_FLAGS}" -DCMAKE_CXX_FLAGS="${ARMCC_FLAGS}" -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON -DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=armv7 ../tensorflow/lite/examples/minimal cmake --build . -j16
4. STM32程序编写
(1)使用CUBE MX配置工程(对应所选择的MCU)
配置通信相关总线
配置PWM
配置FreeRTOS
生成代码
(2)编写PWM灯带、ICM20608、AP3216C相关驱动:(见源码)
(3)编写app_freertos.c:
任务一负责采集信号,数据预处理并通过CAN发送信息
void StartDefaultTask(void *argument) { /* USER CODE BEGIN StartDefaultTask */ uint16_t als[10], ir[10], ps[10]; float temp[10], accx[10], accy[10], accz[10], gyrx[10], gyry[10], gyrz[10]; float tempp, accxx, accyy, acczz, gyrxx, gyryy, gyrzz; int pitch_1, roll_1, temp_1, accy_1; uint16_t fdcan_id = 0x123; uint8_t fdcan_tx_data[8] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5, 0xF6, 0x17, 0x28}; euler_param_t *eulerAngle; for(;;) { uint8_t i = 0; AP_ReadSensor(als, ps, ir); while(i < 6) { eulerAngle = ICM_ReadGyroAccel(&tempp, &accxx, &accyy, &acczz, &gyrxx, &gyryy, &gyrzz); temp[i] = tempp; accx[i] = accxx; accy[i] = accyy; accz[i] = acczz; gyrx[i] = gyrxx; gyry[i] = gyryy; gyrz[i] = gyrzz; i++; // printf("%.4f,%.4f,%.4f,%d,%d ",eulerAngle->pitch, eulerAngle->roll, temp[i], als[i], ir[i]); } //printf("%.4f,%.4f,%.4f,%d,%.4f ",eulerAngle->pitch, eulerAngle->roll, temp[2], als[2], accy[2]); pitch_1 = eulerAngle->pitch * 10000; roll_1 = eulerAngle->roll * 10000; temp_1 = temp[2] * 10000; accy_1 = accy[2] * 10000; (pitch_1 > 0) ? (fdcan_tx_data[0] = 0x01) : (fdcan_tx_data[0] = 0); //first part of a message fdcan_tx_data[1] = pitch_1 / 10000; fdcan_tx_data[2] = pitch_1 / 100 % 100; fdcan_tx_data[3] = pitch_1 % 100; (roll_1 > 0) ? (fdcan_tx_data[4] = 0x01) : (fdcan_tx_data[4] = 0); fdcan_tx_data[5] = roll_1 / 10000; fdcan_tx_data[6] = roll_1 / 100 % 100; fdcan_tx_data[7] = roll_1 % 100; CAN_Transmit(fdcan_id, fdcan_tx_data, 8); vTaskDelay(1);// (temp_1 > 0) ? (fdcan_tx_data[0] = 0x01) : (fdcan_tx_data[0] = 0); //second part of a message fdcan_tx_data[1] = temp_1 / 10000; fdcan_tx_data[2] = temp_1 / 100 % 100; fdcan_tx_data[3] = temp_1 % 100; (als[2] > 0) ? (fdcan_tx_data[4] = 0x01) : (fdcan_tx_data[4] = 0); fdcan_tx_data[5] = als[2] / 10000; fdcan_tx_data[6] = als[2] / 100 % 100; fdcan_tx_data[7] = als[2] % 100; CAN_Transmit(fdcan_id, fdcan_tx_data, 8); vTaskDelay(1);// (accy_1 > 0) ? (fdcan_tx_data[0] = 0x01) : (fdcan_tx_data[0] = 0); //third part of a message fdcan_tx_data[1] = accy_1 / 10000; fdcan_tx_data[2] = accy_1 / 100 % 100; fdcan_tx_data[3] = accy_1 % 100; CAN_Transmit(fdcan_id, fdcan_tx_data, 8); vTaskDelay(1); osDelay(1); } /* USER CODE END StartDefaultTask */ }
任务二负责监听返回报文并将结果发送给任务三
void StartTask02(void *argument) { /* USER CODE BEGIN StartTask02 */ /* Infinite loop */ for(;;) { if (CAN_Receive() == 1) { printf(" mode is %d", can_rx_data.RX_Data[0]); printf(" "); } CAN_SetReceiveFlag(0); if(xQueueSend(myQueue01Handle, &can_rx_data.RX_Data[0], 1) != pdTRUE) { xQueueReset(myQueue01Handle); } osDelay(1); } /* USER CODE END StartTask02 */ }
任务三负责根据不同结果控制灯带
void StartTask03(void *argument) { /* USER CODE BEGIN StartTask03 */ /* Infinite loop */ uint8_t mode = 0; uint8_t rx_buf; for(;;) { if(xQueueReceive(myQueue01Handle, &rx_buf, 1) != pdTRUE) continue; mode = rx_buf + 1; switch (mode) { case 1: for(uint8_t s = 0; s < 3; s++){ WS281x_TheaterChaseRainbow(1); } break; case 2: for(uint8_t s = 0; s < 3; s++){ WS281x_TheaterChaseRainbowred(2); } break; case 3: WS281x_TheaterChase(128, 200); break; case 4: for(uint8_t s = 0; s < 1; s++){ WS281x_RainbowCycle(1); } break; case 5: for(uint8_t s = 0; s < 3; s++){ WS281x_ColorWipe(120, 2); } break; case 6: for(uint8_t s = 0; s < 3; s++){ WS281x_Rainbow(2); } break; } osDelay(1); } /* USER CODE END StartTask03 */ }
四. 最终效果及源码
演示视频:
【IMX6ULL嵌入式AI项目:模拟车载自适应氛围灯(附源码)-哔哩哔哩】
【IMX6ULL嵌入式AI项目:模拟车载自适应氛围灯(附源码)】
gitee仓库:imx6ull-tensorflow-lite: imx6ull嵌入式AI模型部署项目。实现模拟车载氛围灯。使用STM32MP157-M4(可替换为其它低端MCU)充当氛围灯控制器,使用imx6ull充当网关部署Tensorflow Lite分类模型。利用STM32开发板自带ICM_20608和AP3216C采集位姿与环境光数据,将采集数据通过CAN发送至网关,输入神经网络模型,运行推断并返回分类结果,实现模拟汽车不同环境行驶状态下的氛围灯效果。