最近正在入门stm32开发,进行一个简单的蓝牙图片显示终端和温湿度计搭建,在这期间碰到了不少有意思的事情,遂分享之。(进入大学之后,可支配的时间比高中多了不少,我也有更多的精力可以放在自己的爱好上了。)
1 软硬件概况
主控:STM32f103c8t6, 72MHz, ARM-Cortex M3内核,使用一块32.768的晶振作为HSE
片上系统:FreeRTOS v11
液晶显示屏:128 * 128 TFT屏,由st7735s驱动,走SPI协议
温湿度传感器:AHT20,走I2C协议
蓝牙模块:DX-BT24,UART串口
开发环境:HAL库,Keil v5,cmsis接口
2 湿度传感器模块原理和代码 AHT20通过探测聚合物的水合程度来测量相对湿度(即绝对湿度与当前环境温度下水蒸气达到饱和的绝对湿度之比)。
传感器总线I2C通信协议时序:
ATH20的主机与从机通讯命令格式:
响应数据的第7字节为CRC校验数据,在不追求数据准确性的情况下可以直接忽略。Data0到Data2为湿度数据,Data2到Data4为温度数据,解析出来的数据值均为uint32_t
。注意Data2的高4位为湿度数据的最低4位,Data2的低4位为湿度数据的最高4位,中间进行截断,这一点在AHT20的产品规格书上没有明确指出。
还需注意的是,传感器操作需要发出I2C指令并等待从设备应答,指令的发出和响应必须严格对应,因此这类操作并不是线程安全的,应该加锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 #include "stm32f1xx_hal.h" #include "i2c.h" #include <stdio.h> #include <stdint.h> #include "gyconsole.h" #include "cmsis_os.h" #include "AHT20.h" #define AHT20_I2C_PORT hi2c1 #define AHT20_ADDRESS 0x71 static osSemaphoreId_t SemaphoreHandle = NULL ;void AHT20_Init () { osDelay(50 ); uint8_t initCmdBuf[1 ] = {0x71 }; uint8_t initResponseBuf[1 ]; HAL_I2C_Master_Transmit(&AHT20_I2C_PORT, AHT20_ADDRESS, initCmdBuf, 1 , HAL_MAX_DELAY); osDelay(50 ); HAL_I2C_Master_Receive(&AHT20_I2C_PORT, AHT20_ADDRESS, initResponseBuf, 1 , HAL_MAX_DELAY); if ((initResponseBuf[0 ] & 0x18 ) != 0x18 ) { gylog("init failed %x" , initResponseBuf[0 ]); } const osSemaphoreAttr_t SemaphoreAttr = { .name = "AHT20" , .cb_mem = NULL , .cb_size = 0 , }; SemaphoreHandle = osSemaphoreNew(1 , 1 , &SemaphoreAttr); osDelay(10 ); }void AHT20_MeasureData (float *temperature, float *humidity) { osSemaphoreAcquire(SemaphoreHandle, osWaitForever); uint8_t measureCmdBuf[3 ] = {0xac , 0x33 , 0x00 }; uint8_t measureResBuf[8 ] = {0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 }; HAL_I2C_Master_Transmit(&AHT20_I2C_PORT, AHT20_ADDRESS, measureCmdBuf, 3 , HAL_MAX_DELAY); osDelay(80 ); HAL_I2C_Master_Receive(&AHT20_I2C_PORT, AHT20_ADDRESS, measureResBuf, 8 , HAL_MAX_DELAY); while (measureResBuf[0 ] & 0x01 == 0x01 ) { osDelay(10 ); HAL_I2C_Master_Receive(&AHT20_I2C_PORT, AHT20_ADDRESS, measureResBuf, 8 , HAL_MAX_DELAY); } uint32_t humidity_raw = 0 ; humidity_raw |= (uint32_t )measureResBuf[1 ] << 12 ; humidity_raw |= (uint32_t )measureResBuf[2 ] << 4 ; humidity_raw |= (uint32_t )((measureResBuf[3 ] & 0xf0 ) >> 4 ); uint32_t temperature_raw = 0 ; temperature_raw |= ((uint32_t )(measureResBuf[3 ] & 0x0f ) << 16 ); temperature_raw |= (uint32_t )measureResBuf[4 ] << 8 ; temperature_raw |= (uint32_t )measureResBuf[5 ]; *humidity = (float )humidity_raw / (1 << 20 ); *temperature = (float )temperature_raw / (1 << 20 ) * 200.0f - 50.0f ; osSemaphoreRelease(SemaphoreHandle); }
3 遇到的问题 3.1 可变参数函数sprintf
的二次封装 我的这个项目包含了一个128*128的液晶屏作为console(日志系统的一部分),对外输出可供阅读的调试信息。我希望对sprintf
进行二次封装,将st7735s的LCD驱动逻辑放入封装内,来实现在液晶屏幕上显示格式化之后的字符串。但是sprintf
与常规函数有很大不同, 它的形参数量是可变的,无法使用常规的函数定义方式实现。目前能够使用的解决方案有如下两种:
一种方案是采用C99中的预定义宏##__VA_ARGS__
,来在预处理阶段实现参数列表的可变。这里的##
是连接符,用来处理参数个数为零的边界情况,当参数个数为零的时候删除其前面一个逗号(即token为空时,不进行连接),确保最终文本符合C语法。
1 2 3 #define gylog(format, ...) fprintf(stdout, format, ##__VA_ARGS__) #define gylog(format, args...) fprintf (stdout, format, args)
第二种方案用到了stdarg库中的va_list
,va_list
本质上是一个指针,va_start
会把指针指向参数栈顶部。这种方案下要使用vsprintf
代替sprintf
来接收va_list
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void gylog (const char *format, ...) { osSemaphoreAcquire(SemaphoreHandle, osWaitForever); char result[18 ]; va_list vaList; va_start(vaList, format); vsprintf (result, format, vaList); va_end(vaList); ST7735_WriteString(logCurrentX, logCurrentY, result, Font_7x10, rgbToInt(0 , 0 , 0 ), rgbToInt(255 , 172 , 7 )); osSemaphoreRelease(SemaphoreHandle); }
3.2 vTaskList显示问题 FreeRTOS附带了一个vTaskList()
函数来方便开发者查看当前所有任务(线程)的状态,提供的信息包括任务优先级、栈剩余(历史最低值)等。这个函数的输出字符串中包含ASCII制表符,但是我的日志系统不支持使用制表符进行排版,于是出现下图所示的乱码,影响美观:
解决方法很简单,把相应制表符替换成空格即可,此处省略不表。
3.3 链接期间出错问题 1 2 Playground\Playground.axf: Error: L6200E: Symbol SysTick_Handler multiply defined (by port.o and cmsis_os2.o).
经过仔细排查,应该是STM32CubeMX的bug,只好修改其生成的freertos文件,删除重定义,苟一下。
3.4 莫名其妙的汇编语言错误 1 2 ..\Middlewares\Third_Party\FreeRTOS\Source\portable\RVDS\ARM_CM3\port.c(392): error: A1586E: Bad operand types (UnDefOT, Constant) for operator (
定位到出错的地方后,发现这个函数来头不小,它负责PendSV中断服务,管理同级task之间的上下文切换。PendSV可被优先级更高的ISR打断,确保系统的实时性,由#configMAX_SYSCALL_INTERRUPT_PRIORITY
指定。
对mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
(代表4U)稍作修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 __asm void xPortPendSVHandler ( void ) { extern uxCriticalNesting; extern pxCurrentTCB; extern vTaskSwitchContext; PRESERVE8 mrs r0, psp isb ldr r3, =pxCurrentTCB ldr r2, [r3] stmdb r0!, {r4-r11} str r0, [r2] stmdb sp!, {r3, r14} mov r0, #4 msr basepri, r0 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 ldmia sp!, {r3, r14} ldr r1, [r3] ldr r0, [r1] ldmia r0!, {r4-r11} msr psp, r0 isb bx r14 nop }
4.蓝牙图传 4.1 效果图 蓝牙图传的核心实现由两台设备构成:发送端和接收端。这里的发送端是PC,接收端是STM32。
4.2 浏览器端的实现 在了解到浏览器端也有蓝牙api之后,决定上位机使用h5开发。用户选择一张图片,系统通过canvas对图片进行采样,得到128*128的RGBA矩阵,之后进行色彩空间转换,将32位的RGBA制式转换为RGB565制式。随后通过Bluetooth API发送到STM32。
下面代码展示了蓝牙初始化、连接设备、建立GATT、监听characteristic等过程。需要注意requestDevice
必须由用户操作触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const initBluetooth = function ( ) { let type = { service : 0xffe0 , writeCharacteristic : 0xffe1 , notifyCharacteristic : 0xffe1 , } let isBleSupported = navigator && "bluetooth" in navigator if (isBleSupported) { navigator.bluetooth .getAvailability ().then (async () => { const device = await navigator.bluetooth .requestDevice ({ filters : [{ services : [type.service ] }], }); if (device) { const server = await device.gatt .connect (); const service = await server.getPrimaryService (type.service ); const characteristic = await service.getCharacteristic ( type.notifyCharacteristic ); await characteristic.startNotifications (); writeCharacteristic = await service.getCharacteristic ( type.writeCharacteristic ); } else { alert ("连接到设备失败" ) } }) } else { alert ("设备或浏览器不支持蓝牙" ) } }
下面代码展示了图片的发送过程。一张以RGB565格式编码的128*128位图,一共占用32KB的存储空间,由于BT-24模块的MTU只有253字节,所以我对图片采取了分块发送的策略,把单个数据包的大小控制在了160字节以内。发送出去的数据遵循一个简单且固定的数据包格式。0x23是一个初始化指令,单片机收到该指令后会停止所有绘制任务,等待接收0xf1绘制指令。0xf1指令有两个参数,分别是图块左上角的x坐标和y坐标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const sendImg = async function ( ) { await utils.sendHex (Uint8Array .from ([0x23 , 0 ])) for (let rowNum = 0 ; rowNum < config.LCD_HEIGHT ; rowNum++) { let bufferSemi1 = []; let bufferSemi2 = []; for (let i = rowNum * config.LCD_WIDTH ; i < rowNum * config.LCD_WIDTH + config.LCD_WIDTH ; i++) { let uint16Data = utils.rgbToInt (global .rgbList [i][0 ], global .rgbList [i][1 ], global .rgbList [i][2 ]) if (i < rowNum * config.LCD_WIDTH + config.LCD_WIDTH / 2 ) { bufferSemi1 = [...bufferSemi1, uint16Data >> 8 , uint16Data & 0xff ] } else { bufferSemi2 = [...bufferSemi2, uint16Data >> 8 , uint16Data & 0xff ] } } await utils.sendHex (Uint8Array .from ([0xf1 , 0 , 0 , rowNum, ...bufferSemi1])) await utils.sendHex (Uint8Array .from ([0xf1 , 0 , config.LCD_WIDTH / 2 , rowNum, ...bufferSemi2])) await utils.sleep (config.sleepMs ) } }
兼容性问题:Web Bluetooth API目前并不是W3C标准,目前只有Chromium系的浏览器支持得较好,Firefox和Safari更是全线不支持。
4.3 接收端的实现 借助freertos的消息队列API,在系统上电并完成初始化后,初始化一个128*4的消息队列。在接收到串口数据后,往消息队列中插入数据(ISR中完成)。使用osMessageQueuePut
实现外设中断与操作系统的沟通。
1 2 3 4 5 6 7 8 9 10 11 12 13 void HAL_UARTEx_RxEventCallback (UART_HandleTypeDef *huart, uint16_t Size) { if (huart == &BT24_UART_PORT) { osMessageQueuePut(BT24MsgQueueHandle, receiveDataBuf, NULL , 0 ); HAL_UARTEx_ReceiveToIdle_DMA(&BT24_UART_PORT, receiveDataBuf, BT24_SINGLE_MSG_SIZE); __HAL_DMA_DISABLE_IT(&BT24_DMA_RX, DMA_IT_HT); } }