前边的两篇笔记将STM32总线上与外部的通信协议I2C和SPI,本笔记着重讲解如何使用。
我们使用这些总线上的通信协议和外部模块进行通信时,STM32外设的配置要和外部模块的一致,这样才能通讯同步,得到正确的数据。而这些外部模块一般厂商都会提供手册和驱动,我们只需要移植过来,将引脚和基本的通讯配置好,详细的收发函数大部分都已由厂商实现好了。我们只需要阅读厂商提供的代码和文档来了解针对不同模块的指令。如常用的FLASH芯片是使用QSPI通信的,支持队列操作,我手上的开发板的LCD是使用SPI通信的,一些传感器是使用I2C通信的。我们根据芯片数据手册分配的引脚和外部模块给出的详细指令通讯协议在CubeMX中配置好即可。
BH1750FVI是日本罗姆(ROHM)半导体生产的数字式环境光传感IC。BH1750环境光传感器内置16位的模数转换器,它能够直接输出一个数字信号,不需要再做复杂的计算。这是一种容易使用简易电阻器的版本,通过计算电压,来获得有效的数据。这款环境光传感器能够直接通过光度计来测量。当物体在均匀的光照下它能够在每平方米获得1lux的光通量,它们的光强度是1lux。有时为了充分利用光源,你可以增加一个光源的反射装置。那样在某些方向就能获得更多的光通量,以增加被照表面的亮度。
功能测试
BH1750FVI支持单次或连续两种测量模式,每种测量模式又提供了0.5lux、1lux、4lux三种分辨率供选择。分辨力越高,一次测量所需的时间就越长。在单次测量模式时,每次测量之后传感器都自动进入Power Down模式。
连续模式分辨率从高到低的指令为:0x10; 0x11; 0x13
单次模式分辨率从高到低的指令为:0x20; 0x21; 0x23
主要特性:
I2C数字接口,支持速率最大400Kbps
输出量为光照度(Illuminance)
测量范围1~65535 lux,分辨率最小到1lux
低功耗(Power down)功能
屏蔽50/60Hz市电频率引起的光照变化干扰
支持两个I2C地址,通过ADDR引脚选择
较小的测量误差(精度误差最大值+/-20%)
由于要串口打印数据,所以将I2C1引脚配置为PB6/7;使用两个GPIO引脚连接灯进行指示。参数都使用默认的。
采用内部时钟(默认)配置系统时钟最高80MHz
I2C1时钟为16MHz
使用默认参数。
主要调用I2C函数(示例):
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
主要函数代码(示例):
/*
连续高分辨率模式精度1lux:0x10
连续高分辨率模式2精度0.5lux:0x11
低分辨率模式:0x13
*/
float ReadBH1750(void)
{
uint8_t temp[1]={
0x11};//连续高分辨率模式2
float lux=0;
uint8_t ReadData[2]={
0};
HAL_I2C_Master_Transmit(&hi2c1, 0x46, temp, 1, 0xff);
HAL_Delay(180);
HAL_I2C_Master_Receive(&hi2c1, 0x47,ReadData, 2, 0xff);
lux=(float)((ReadData[0]<<8)|ReadData[1]);
lux=(double)lux/1.2;
return lux;
}
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
float lux=ReadBH1750();
printf("BH1750:%d\r\n",(int)lux);
if(lux<2||(lux>900&&lux<1100))
HAL_GPIO_WritePin(LED_SW_GPIO_Port,LED_SW_Pin,GPIO_PIN_SET);
else
HAL_GPIO_WritePin(LED_SW_GPIO_Port,LED_SW_Pin,GPIO_PIN_RESET);
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
HAL_Delay(1000);
/* USER CODE END WHILE */
I2C多数是用来采集数据,因为使用两根线,使用从机地址进行寻址通信,所以传输是单向的,我们只需很简单的将我们的芯片作为主机向传感器发送检测指令,等待传感器送回数据即可。编程要点在于,根据外部模块和芯片的原理图合理选择引脚连接,并配置相关参数:
(1) 配置通讯使用的目标引脚为开漏模式;
(2) 使能I2C 外设的时钟;
(3) 配置I2C 外设的模式、地址、速率等参数并使能I2C 外设;
实现数据的收发函数,如本例的ReadBH1750(),将I2C的读写函数组合封装进我们自行编写的函数以实现我们想要的复杂功能。
TFT-LCD 即薄膜晶体管液晶显示器。其英文全称为:Thin Film Transistor-Liquid Crystal Display。TFT-LCD 与无源TN-LCD、STN-LCD 的简单矩阵不同,它在液晶显示屏的每一个象素上都设置有一个薄膜晶体管(TFT),可有效地克服非选通时的串扰,使显示液晶屏的静态特性与扫描线数无关,因此大大提高了图像质量。TFT-LCD 也被叫做真彩液晶显示器。
ST7789V2是一个单芯片TFT-LCD驱动器。该芯片可以直接连接到外部MCU,支持并行8080系列的8位/9位/16位/18位接口,也支持SPI串行通讯接口。 显示数据可以存储在240x320x18bits的片上显示数据RAM中。 它可以在没有外部操作时钟的情况下执行显示数据RAM读写操作,以尽量减少功耗。
并行接口占用外部MCU芯片引脚较多,但其通讯速率较快,一般只在需要高速刷新及MCU资源比较丰富的场合使用。SPI串行通讯接口占用MCU芯片引脚较少,通讯速率相对并行接口较慢,但因其占用MCU引脚资源较少被广泛使用。因此我们也使用SPI通讯的方式和ST7789V2进行通讯。
采用内部时钟(默认)配置系统时钟最高80MHz
根据LCD数据手册,选择4线模式1,数据传输模式为MSB,上升沿采样。根据前面讲述,SPI模式一般选模式0或者模式3,分别是
1、CPOL=0,CPHA=0(空闲时时钟线为低,在第一个时钟边沿即上升沿进行采样)
2、CPOL=1,CPHA=1(空闲时时钟线为高,在第二个时钟边沿即上升沿进行采样)
因为外部芯片的SPI工作模式是固定的,但STM32的工作模式是可以配置的,因此需要将STM32的SPI工作模式配置和外部芯片一致才可以正常通讯,还需要注意外部芯片支持的SPI通讯的速率,STM32设置的SPI通讯速率不能比它高。一般情况下,外部SPI芯片手册中会说明该芯片是在时钟边沿的上升沿采样还是下降沿采样,根据此信息一般STM32会有两种两种工作模式可以满足,选择任意一种即可,一般偏向于选择CPOL=1即空闲时时钟为高的那种,即此处我们选用模式3。
SPI配置好后就可以和ST7789V2芯片进行通讯了,具体发送的命令和需要LCD如何显示那就要看ST7789V2的芯片手册。
主要调用SPI函数(示例):
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout);
主要函数代码(示例):
spi.c
中添加如下函数,并注意在对应头文件中声明
/* USER CODE BEGIN 1 */
uint8_t SPI2_WriteByte(uint8_t* data, uint16_t size)
{
return HAL_SPI_Transmit(&hspi2, data, size, 0xff);
}
/* USER CODE END 1 */
main.c
中添加用户代码
/* USER CODE BEGIN Includes */
#include "LCD.h"
/* USER CODE END Includes */
/* USER CODE BEGIN 2 */
LCD_Init();
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
LCD_Clear(BLACK);//清屏为黑色
LCD_ShowString(5, 10, 240, 32, 32, "BearPi LCDTest");//显示字符串,字体大小32*32
PutChinese_strings(10,150,"你好",0);
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_SET);//点亮LED灯
HAL_Delay(1000);//延时1秒
LCD_Clear(BLUE);//清屏为蓝色
LCD_Draw_Circle(120, 120, 100);//画圆 半径r=100
LCD_Draw_Circle(120, 120, 80);//画圆 半径r=80
LCD_Draw_Circle(120, 120, 60);//画圆 半径r=60
LCD_Draw_Circle(120, 120, 40);//画圆 半径r=40
LCD_Draw_Circle(120, 120, 20);//画圆 半径r=20
LCD_Draw_Circle(120, 120, 1);//画圆 半径r=1
HAL_GPIO_WritePin(LED_GPIO_Port,LED_Pin,GPIO_PIN_RESET);//熄灭LED灯
HAL_Delay(1000);//延时1秒
}
/* USER CODE END 3 */
LCD芯片手册共319页,大部分内容都介绍的是如何操作其中的寄存器以实现显示功能的配置。我们可以直接使用开发板厂商写好的驱动程序使用(网上资料鱼龙混杂,也不一定完全正确,因此需要自己边试边改),掌握芯片的操作原理即可,当需要实现特定的、炫酷的、网上找不到的功能时再继续深入去研究。不要重复造轮子,或者一上来就去啃ST7798V2的芯片手册。
本实验移植的是小熊派开发板厂商提供的示例中的LCD驱动。
DC为命令/数据标志(0为读写命令,1为读写数据);CS为片选,RST为复位
QSPI是Quad SPI的简写,是Motorola公司推出的SPI接口的扩展,比SPI应用更加广泛。在SPI协议的基础上,Motorola公司对其功能进行了增强,增加了队列传输机制,推出了队列串行外围接口协议(即QSPI协议)。QSPI 是一种专用的通信接口,连接单、双或四(条数据线) SPI Flash 存储介质。
该接口可以在以下三种模式下工作:
间接模式:使用 QSPI 寄存器执行全部操作
状态轮询模式:周期性读取外部 Flash 状态寄存器,而且标志位置 1 时会产生中断(如擦除或烧写完成,会产生中断)
内存映射模式:外部 Flash 映射到微控制器地址空间,从而系统将其视作内部存储器采用双闪存模式时,将同时访问两个 Quad-SPI Flash,吞吐量和容量均可提高二倍。
QSPI 可以使用 6 个信号连接Flash,分别是四个数据线BK1_IO0~BK1_IO3,一个时钟输出CLK,一个片选输出(低电平有效)BK1_nCS,它们的作用介绍如下:
本实验:只用到BK1_nCS、CLK、BK1_IO0、BK1_IO1四个信号。
大小:8M(Byte)(128块(Block),每块64K字节,每块16个扇区(Sector),每个扇区4K字节,每个扇区16页,每页256个字节)
特点:Flash芯片内的数据只能由1变0,不能由0变1。
W25Q64Flash工作方式:
1)W25Q64 SPI数据传输时序
W25Q64支持SPI数据传输时序模式0(CPOL = 0、CPHA = 0)和模式3(CPOL = 1、CPHA = 1),模式0和模式3主要区别是当SPI主机硬件接口处于空闲状态时,SCLK的电平状态是高电平或者是低电平。对于模式0来说,SCLK处于低电平;对于模式3来说,SCLK处于高电平。不过,在这两种模式下,芯片都是在SCLK的上升沿采集输入数据,下降沿输出数据。
2)W25Q64数据格式
W25Q64数据格式为数据长度8位大小,先发高位,再发低位。
3)W25Q64传输速度
W25Q64在标准模式下支持80M bit/s速度,快速模式下支持160M bit/s速度,高速模式下支持320M bit/s速度。
通过SPI接口,用标准的SPI协议发送相应指令给flash,然后flash根据命令进行各种相关操作。
更多指令我们可以查看手册,编程要点和前边一样,配置好引脚和参数进行通信,编写基本读写函数进行操作,再在此基础上实现复杂操作。
根据主芯片原理图我们配置引脚如下:
采用内部时钟(默认)配置系统时钟最高80MHz
QSPI_CommandTypeDef;
HAL_StatusTypeDef HAL_QSPI_Command(QSPI_HandleTypeDef *hqspi, QSPI_CommandTypeDef *cmd, uint32_t Timeout);
HAL_StatusTypeDef HAL_QSPI_Transmit(QSPI_HandleTypeDef *hqspi, uint8_t *pData, uint32_t Timeout);
HAL_StatusTypeDef HAL_QSPI_Receive(QSPI_HandleTypeDef *hqspi, uint8_t *pData, uint32_t Timeout);
还有驱动文件中的接口函数。
主要函数代码(示例):
/* USER CODE BEGIN 1 */
uint32_t location1=0;
uint32_t location2=4096;
uint32_t W25xID;
uint16_t ReadData[]={
0};
uint16_t ReadData_1[]={
0};
uint8_t writeData[]={
"welcome to IoT"};
uint8_t writeData_1[]={
"第一个QuadSPI实验"};
/* USER CODE END 1 */
/* USER CODE BEGIN 2 */
hal_spi_flash_read(ReadData,sizeof(writeData),0);//读0位的数据
printf("ReadFlashData:%s\r\n",(char*)ReadData);//打印读出的数据
hal_spi_flash_read(ReadData_1,sizeof(writeData_1),4096);;//读100位的数据
printf("ReadFlashData_1:%s\r\n",(char*)ReadData_1);//打印读出的数据
W25xID=hal_spi_flash_get_id();//读取芯片ID
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)
{
HAL_Delay(100);
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin)==GPIO_PIN_RESET)
{
printf("KEY1 Press \r\n");
printf("W25xID:%x\r\n",W25xID);
/*最小擦除为4096,所以一次就要擦除0-4096位置,然后在0-4096的任意位置写入数据*/
hal_spi_flash_erase_write(writeData, sizeof(writeData),0);
}
}
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)
{
HAL_Delay(100);
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)==GPIO_PIN_RESET)
{
printf("KEY2 Press \r\n");
printf("W25xID:%x\r\n",W25xID);
/*最小擦除为4096,所以一次就要擦除0-4096位置,然后在0-4096的任意位置写入数据*/
hal_spi_flash_erase_write(writeData_1, sizeof(writeData_1),location2);
}
}
}
/* USER CODE END 3 */
对FLASH操作时,一定要注意扇区大小和地址。避免擦除重要数据。
STM32内部Flash有一套自己的操作函数,用时直接调用就好,内部flash的地址起始于0x0800,结束地址为起始地址加上内部flash大小。内部构成主要有:主存储器、系统存储器、OTP区域以及选项字节区域。
主存储器就是平时我们烧写用到的区域,存放我们写的垃圾用户程序,写入前先擦除之前的垃圾;
系统存储器里边固化的是系统代码,也就是俗称的固件,实现引导烧录等系统级功能;
OTP区域只能写入一次,容量为512字节,常用来存储应用程序的加密密钥;
选项字节用于配置FLASH的读写保护、电源管理中的BOR级别、软/硬看门狗功能,共32字节。可通过修改选项控制寄存器修改。
从上述三个示例中可以学到,一些常用的外部模块,已经有很多已经实现好的驱动,我们只需要将MCU和模块的通讯配置好,调用对应的接口函数就能通信,如果需要实现复杂的功能,可以使用已实现的驱动文件,或者自己查阅手册进行实现。这也是以后自己做产品时的思路。