文章目录
1. 准备工作
1.1. 所用硬件
STM32F103 普中-准端-Z100,主控 STM32F103ZET6.
1.2. IIC简介
IIC(Inter Integrated Circuit)
- 由 PHILIPS 公司开发
- 需要两根线(
数据线SDA
和时钟SCL
) - 双向传送
- 高速IIC 可达 400kbps 以上
三种类型信号(看看就行,这里用的硬件IIC,也不用自己写)
- 开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据;
- 结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据;
- 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。
起始信号是必需的,结束信号和应答信号,都可以不要。
1.2. 生成工程
1.2.1. 创建工程选择主控
1.2.2. 系统配置
配置时钟源
配置debug模式(如果需要ST-Link下载及调试可以勾选)
配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)
1.2.3. 配置工程目录
1.2.4. IIC配置
然后生成代码。
2. 读写EEPROM实验(AT24C02)
EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。
2.1. AT24C02简介
2.1.1. AT24C02原理图
- A0,A1,A2:硬件地址引脚
- WP:写保护引脚,接高电平只读,接地允许读和写
- SCL和SDA:IIC总线
24C02后面的 02 表示的是可存储 2Kbit 的数据,转换为字节的存储量为2*1024/8 = 256byte;256个字节一共分为32页,每页8个字节。
2.1.2. 设备地址
在AT24C02的参考手册第9页 可以看到下图
AT24C设备地址为如下:
- 前四位固定 1010
- A2、A1、A0为由管脚电平决定。此处原理图都接地,默认为000。
- 最后一位表示读操作还是写操作。读地址为0xA1,写地址为0xA0。
结论:
- 写24C02的时候,从器件地址为10100000(0xA0)
- 读24C02的时候,从器件地址为10100001(0xA1)
片内地址寻址:
芯片寻址可对 内部256个字节 中的任一个进行 读/写操作,其寻址范围为00~FF,共256个寻址单位。
2.2. 读写时序
写一个字节
从时序图上可以看出(上面是MCU的信号,下面是存储芯片的信号),写一个字节数据的操作顺序为:
- MCU 先给芯片发送一个开始信号(START)
- 开始信后之后的第一个字节,发送要写入的设备地址(DEVICE ADDRESS)(注意,因为总线上可能由多个设备,是根据设备地址选择不同的设备的),然后发送写数据命令(0xA0),然后等待应答信号(ACK)
- 发送数据的存储地址。一共有256个字节的存储空间,地址从0x00~0xFF,想存到哪个地址,就发哪个地址
- 发送要存储的数据,发送完成之后MCU会收到应答信号
- 数据发送完成之后,发送结束信号(STOP)停止总线。
读一个字节
从时序图上可以看出(上面是MCU的信号,下面是存储芯片的信号),读一个字节数据的操作顺序为:
- MCU 先给芯片发送一个开始信号(START)
- 开始信号之后的第一个字节,发送要读取的设备地址(DEVICE ADDRESS)(注意,因为总线上可能由多个设备,是根据设备地址选择不同的设备的),然后发送写数据命令(0xA0),并发送要读取的数据地址(WORD ADDRESS),然后等待应答信号(ACK)
- 再次发送开始信号(START)
- 开始信号之后的第一个字节,发送要读取的设备地址(DEVICE ADDRESS),发送读取数据命令(0xA1)
- 此时,24C02会自动给MCU发送数据。
- MCU发送结束信号(STOP)停止总线。
写一页数据
时序图和写单个字节差不多,只是每个字节写完之后存储器都会给MCU发送应答信号,之后继续发送下一个字节,写完之后,MCU发送停止信号即可。
256个字节一共分为32页,每页8个字节。AT24C02页写入只支持8个byte,所以需要分32次写入。如果按照上述时序连续写入8个字节后,会重复的继续往该页写数据。(当然也可以 往256个地址中分别写入一个字节。。。)
连续读数据
时序图和都单个字节差不多,存储器给MCU发送完每个字节,MCU要发送应答信号给存储器,直到MCU发送停止信号。且读数据没有8个字节的限制。
2.3. 代码实现
虽然时序看起来很复杂,但是不用担心,很多都已经有实现了。
在生成的工程中,打开stm32f1xx_hal.h
,可以看到已经生成了轮询,中断和DMA三种控制方式的代码。
我们只看轮询的,其他的也都差不多,只是应用场景不一样。
// 作为主机 发送数据
// 参数:iic接口、设备地址、发送的数据、数据长度、超时时间
HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 作为主机 接收数据
// 参数:iic接口、设备地址、存储读取到的数据、数据长度、超时时间
HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 作为从机 发送数据
// 参数:iic接口、设备地址、发送的数据、数据长度、超时时间
HAL_I2C_Slave_Transmit(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 作为从机 接收数据
// 参数:iic接口、设备地址、存储读取到的数据、数据长度、超时时间
HAL_I2C_Slave_Receive(I2C_HandleTypeDef *hi2c, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 直接发送两个字节数据(就用于我们现在的情况,发送命令 + 发送数据)
// 参数:iic接口、设备地址、发送的数据1、发送的数据2、数据长度、超时时间
HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 直接发送两个字节数据,并接受数据(就用于我们现在的情况,发送命令 + 接收数据)
// 参数:iic接口、设备地址、发送的数据1、发送的数据2、存储读取到的数据、数据长度、超时时间
HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
// 查询设备是否就绪
HAL_I2C_IsDeviceReady(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint32_t Trials, uint32_t Timeout);
我们可以直接看HAL_I2C_Mem_Write
、HAL_I2C_Mem_Read
,刚好可以满足我们这里需要发送 指令 + 地址
的情况,其在发送或者读取数据的过程中,地址还可以自己增加,很方便。
在i2c.h
中声明如下代码
/* USER CODE BEGIN Prototypes */
#define AT24C02_ADDR_WRITE 0xA0 // 写命令
#define AT24C02_ADDR_READ 0xA1 // 读命令
uint8_t At24c02_Write_Byte(uint16_t addr, uint8_t* dat);
uint8_t At24c02_Read_Byte(uint16_t addr, uint8_t* read_buf);
uint8_t At24c02_Write_Amount_Byte(uint16_t addr, uint8_t* dat, uint16_t size);
uint8_t At24c02_Read_Amount_Byte(uint16_t addr, uint8_t* recv_buf, uint16_t size);
/* USER CODE END Prototypes */
在i2c.c
中添加如下代码
/* USER CODE BEGIN 1 */
#include <string.h>
/**
* @brief AT24C02任意地址写一个字节数据
* @param addr —— 写数据的地址(0-255)
* @param dat —— 存放写入数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Write_Byte(uint16_t addr, uint8_t* dat)
{
HAL_StatusTypeDef result;
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, 1, 0xFFFF);
HAL_Delay(5); // 写一个字节,延迟一段时间,不能连续写
return result;
}
/**
* @brief AT24C02任意地址读一个字节数据
* @param addr —— 读数据的地址(0-255)
* @param read_buf —— 存放读取数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Read_Byte(uint16_t addr, uint8_t* read_buf)
{
return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, addr, I2C_MEMADD_SIZE_8BIT, read_buf, 1, 0xFFFF);
}
/**
* @brief AT24C02任意地址连续写多个字节数据
* @param addr —— 写数据的地址(0-255)
* @param dat —— 存放写入数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Write_Amount_Byte(uint16_t addr, uint8_t* dat, uint16_t size)
{
uint8_t i = 0;
uint16_t cnt = 0; // 写入字节计数
HAL_StatusTypeDef result; // 返回是否写入成功
/* 对于起始地址,有两种情况,分别判断 */
if(0 == addr % 8)
{
/* 起始地址刚好是页开始地址 */
/* 对于写入的字节数,有两种情况,分别判断 */
if(size <= 8)
{
// 写入的字节数不大于一页,直接写入
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, size, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
else
{
// 写入的字节数大于一页,先将整页循环写入
for(i = 0; i < size/8; i++)
{
HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], 8, 0xFFFF);
// 一次写入了八个字节,延迟久一点
HAL_Delay(20); // 写完八个字节,延迟久一点
addr += 8;
cnt += 8;
}
// 将剩余的字节写入
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], size - cnt, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
}
else
{
/* 起始地址偏离页开始地址 */
/* 对于写入的字节数,有两种情况,分别判断 */
if(size <= (8 - addr%8))
{
/* 在该页可以写完 */
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, size, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
else
{
/* 该页写不完 */
// 先将该页写完
cnt += 8 - addr%8;
HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, dat, cnt, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
addr += cnt;
// 循环写整页数据
for(i = 0;i < (size - cnt)/8; i++)
{
HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], 8, 0xFFFF);
HAL_Delay(20); // 写完八个字节,延迟久一点
addr += 8;
cnt += 8;
}
// 将剩下的字节写入
result = HAL_I2C_Mem_Write(&hi2c1, AT24C02_ADDR_WRITE, addr, I2C_MEMADD_SIZE_8BIT, &dat[cnt], size - cnt, 0xFFFF);
HAL_Delay(20); // 写完八个字节(最多八个字节),延迟久一点
return result;
}
}
}
/**
* @brief AT24C02任意地址连续读多个字节数据
* @param addr —— 读数据的地址(0-255)
* @param dat —— 存放读出数据的地址
* @retval 成功 —— HAL_OK
*/
uint8_t At24c02_Read_Amount_Byte(uint16_t addr, uint8_t* recv_buf, uint16_t size)
{
return HAL_I2C_Mem_Read(&hi2c1, AT24C02_ADDR_READ, addr, I2C_MEMADD_SIZE_8BIT, recv_buf, size, 0xFFFF);
}
/* USER CODE END 1 */
在main.c
中添加如下代码
int main(void)
{
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* Configure the system clock */
SystemClock_Config();
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();
// 单个字节 读写测试
uint8_t simple_write_dat = 0xa5; // 一个字节
uint8_t simple_recv_buf = 0;
if(HAL_OK == At24c02_Write_Byte(10, &simple_write_dat)){
printf("Simple data write success \r\n");
} else {
printf("Simple data write fail \r\n");
}
HAL_Delay(50); // 写一次和读一次之间需要短暂的延时
if(HAL_OK == At24c02_Read_Byte(10, &simple_recv_buf)){
printf("Simple data read success, recv_buf = 0x%02X \r\n", simple_recv_buf);
} else {
printf("Simple data read fail \r\n");
}
printf("--------------------- \r\n");
// 单个字节读写 测试结束
// 浮点数 读写测试
union float_union{
float float_write_dat; // 浮点数占4个字节
double double_write_dat; // 双精度浮点数占8个字节
uint8_t buf[8]; // 定义 8个字节 的空间
};
union float_union send_float_data; // 用来发送
union float_union rev_float_data; // 用来接收
// 先测试第一个 浮点数
send_float_data.float_write_dat = 3.1415f;
if(HAL_OK == At24c02_Write_Amount_Byte(20, send_float_data.buf, 4)){
printf("Float data write success \r\n");
} else {
printf("Float data write fail \r\n");
}
HAL_Delay(50);
if(HAL_OK == At24c02_Read_Amount_Byte(20, rev_float_data.buf, 4)){
// 默认输出六位小数
printf("Float data read success, recv_buf = %f \r\n", rev_float_data.float_write_dat);
} else {
printf("Float data read fail \r\n");
}
// 测试第二个 双精度浮点数
send_float_data.double_write_dat = 3.1415f;
if(HAL_OK == At24c02_Write_Amount_Byte(20, send_float_data.buf, 8)){
printf("Double data write success \r\n");
} else {
printf("Double data write fail \r\n");
}
HAL_Delay(50);
if(HAL_OK == At24c02_Read_Amount_Byte(20, rev_float_data.buf, 8)){
// 最多15位小数
printf("Double data read success, recv_buf = %.15f \r\n", rev_float_data.double_write_dat);
} else {
printf("Double data read fail \r\n");
}
printf("--------------------- \r\n");
// 浮点数读写测试 测试结束
// 连续数据读写测试
uint8_t write_dat[22] = {
0}; // 22个字节
uint8_t recv_buf[22] = {
0};
printf("正在往数组中填充数据... \r\n");
for(int i = 0; i < 22; i++){
write_dat[i] = i;
printf("%02X ", write_dat[i]);
}
printf("\r\n 数组中数据填充完毕... \r\n");
if(HAL_OK == At24c02_Write_Amount_Byte(0, write_dat, 22)){
printf("24c02 write success \r\n");
} else {
printf("24c02 write fail \r\n");
}
HAL_Delay(50); // 写一次和读一次之间需要短暂的延时
if(HAL_OK == At24c02_Read_Amount_Byte(0, recv_buf, 22)){
printf("read success \r\n");
for(int i = 0; i < 22; i++) {
printf("0x%02X ", recv_buf[i]);
}
} else {
printf("read fail\r\n");
}
// 连续数据读写 测试结束
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
因为每次发送或者接收只能按照一个字节的单位进行,因此对于 uint8_t 类型的整数没什么问题。但是对于浮点数等,占用多个字节的,就可以通过共用体的方式进行。
此方法在许多应用场景中都有应用,比如串口发送浮点数,也可以用这样的方式进行。
效果验证
编译、烧录
用串口助手观察现象。
3. 0.96寸OLED显示实现
3.1. OLED简介(SSD1306)
四线OLED采用IIC通信。该模块集成了 SSD1306存储芯片
。
从模块数据手册可以看到下图。GDDRAM是一个位映射静态RAM,保存要显示的位模式。RAM的大小为128 x 64位,RAM分为八页,从第0页到第7页,用于单色128x64点阵显示器。
存储大小刚好对应了我们屏幕的分辨大小(128*64)。简单来说,就是只要在对应的存储位上存储有效电平,对应的像素点就可以亮起来。
3.2. 代码实现
具体的显示方式和上个章节的差不多,这里直接放代码,抄来用即可。
因为这里的代码比较多,因此我们另外建个文件夹放我们的代码。
- 在工程目录下,创建
icode
文件夹,用来存放我们自己的代码。 - 在
icode
文件夹下,创建OLED
文件夹,存放oled相关代码。 - 在
OLED
文件夹下, 创建如下几个文件:oled.c
、oled.h
、oledfont.h
、bmp.h
。
oled.c:源文件
oled.h:头文件
oledfont.h:字库文件
bmp.h:图片库文件
把源文件添加进去
把头文件路径添加进去
代码如下(我总觉得我手上的东西有问题,等有个靠谱的再写把)
oled.h
oled.c
oledfont.h
bmp.h