carry
发布于 2024-06-14 / 45 阅读
0
0

使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容博客作业7 其他功能:ADC模数转换、CAN通信和FLASH擦写

使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容博客作业7 其他功能:ADC模数转换、CAN通信和FLASH擦写

零、 创新点

《嵌入式技术与基础 第六版》原书教材随书附赠了一块stm32L431的开发板,配套内容都是以此为基础打造的,甚至作者还专门为此开发了一套ide,但是:

  • 现在stm32主流的教程生态用的都是stm32f103c8t6这款芯片,原书的教程虽然能很好的为你解释嵌入式开发的原理,但是要进一步学习,你还是不得不寻找其他教程,购买主流的开发板
  • 作者的ide完全是出于本书学习设计的,对于实际开发毫无意义,若将来想做点嵌入式,还需要重新学习一套开发栈
  • 全新套书卖100。作为抠门带血生,肯定要考虑二手的,但是二手书通常不会带有原书的附件。单买开发板,某宝60,某鱼甚至没有。起码截至我使用该书学习的时间(2024年3月)是这样的,后期使用此书的前辈多了,估计二手市场会变好。
  • 最重要的是,我已经有一个stm32f103c8t6的开发板了

综上,无论是从学习角度还是省钱角度,使用stm32f103c8t6完成《嵌入式技术与基础 第六版》相关内容都是非常合适的

这套博客是广州大学计算机学院嵌入式系统课程的作业,我用stm32f103c8t6开荒,也是为了后来的同学做贡献

一、 ADC模数转换

1. 简介

STM32的ADC(Analog-to-Digital Converter)即模拟数字转换器,是微控制器中一个重要的外设,它能够将连续变化的模拟信号转换成离散的数字信号,以便于处理器进行处理。STM32系列微控制器内置了高性能的ADC模块,下面是对STM32 ADC的详细介绍:

基本特性:

  • 分辨率:STM32的ADC通常是12位分辨率,意味着它可以将输入的模拟电压分为4096(即2^12)个等间隔的量化级别。
  • 架构:采用逐次逼近寄存器(SAR, Successive Approximation Register)型ADC,这是一种常见的ADC架构,通过比较和反馈机制逐步逼近并确定最终的数字值。
  • 通道数量:STM32F10X系列为例,最多有3个独立的ADC控制器,总共支持18个通道,可测量16个外部和2个内部信号源。外部通道通常与GPIO引脚关联,而内部通道可能包括连接到温度传感器、参考电压等的通道。
  • 转换模式:支持单次转换、连续转换、扫描模式(可以依次自动转换多个通道)和间断模式(由外部事件触发)。
  • 采样时间与转换时间:ADC的转换时间由采样时间和ADC时钟周期决定,转换时间=采样时间+固定的转换周期(如12.5个ADC时钟周期)。采样时间可以通过配置参数调整,以适应不同信号特性的需求。
  • 时钟:ADC的时钟(ADCCLK)通常由APB2总线时钟经分频得到,最大频率可达72MHz(具体取决于MCU型号和配置)。
  • 电源与输入范围:ADC模块的工作电压范围为2.4V至3.6V,输入信号的电压范围则应位于VREF-与VREF+之间,其中VREF可以是内部参考电压或外部提供的参考电压。

特殊功能:

  • 双ADC模式:在某些系列中,如STM32F103,ADC1和ADC2可以组合工作在双ADC模式下,从而提高采样率。
  • 注入转换:允许在正常连续转换序列中插入一次高优先级的转换。
  • 模拟看门狗:可以设置阈值,监控输入信号,当信号超出设定范围时产生中断。
  • 中断和DMA:转换结束、注入转换结束或模拟看门狗事件可以触发中断,同时支持DMA(直接内存访问)传输结果,减少CPU干预,提高效率。

配置与使用:

配置STM32的ADC通常涉及以下步骤:

  1. 时钟配置:确保ADC的时钟已经开启。
  2. 通道选择与配置:根据需要选择并配置ADC通道,包括采样时间、通道的输入模式等。
  3. 转换模式设置:选择合适的转换模式,如单次、连续或扫描模式。
  4. 中断与DMA配置:如果需要,配置相关的中断和DMA传输。
  5. 启动转换:通过软件命令或外部触发(如定时器)启动ADC转换。

2. HAL库中的代码

初始化与配置函数:

  • HAL_ADC_Init(ADC_HandleTypeDef* hadc)​: 初始化给定的ADC外设,根据ADC_HandleTypeDef​结构体中的配置进行设置。
  • HAL_ADC_ConfigChannel(ADC_HandleTypeDef* hadc, ADC_ChannelConfTypeDef* sConfig)​: 配置ADC的一个通道,包括通道编号、采样时间、单次/连续转换模式等。
  • HAL_ADCEx_Calibration_Start(ADC_HandleTypeDef* hadc)​: 对支持校准的ADC进行校准(注意,并非所有STM32系列都支持此功能)。

启动与停止转换函数:

  • HAL_ADC_Start(ADC_HandleTypeDef* hadc)​: 启动ADC进行连续转换,但不使用中断。
  • HAL_ADC_Start_IT(ADC_HandleTypeDef* hadc)​: 启动ADC并使能中断,在每次转换完成后触发中断。
  • HAL_ADC_Stop(ADC_HandleTypeDef* hadc)​: 停止正在进行的ADC转换。

中断与回调函数:

  • HAL_ADC_IRQHandler(ADC_HandleTypeDef* hadc)​: 中断服务例程(ISR),用于处理ADC中断,一般不需要用户直接调用,而是由HAL库内部调用来处理中断。
  • HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)​: ADC转换完成回调函数,每次转换完成时由HAL库调用,用户在此函数中可以获取转换结果并进行后续处理。

读取转换结果:

  • HAL_ADC_GetValue(ADC_HandleTypeDef* hadc)​: 获取最近一次ADC转换的结果。
  • HAL_ADC_GetConversionValue(ADC_HandleTypeDef* hadc)​: 类似于HAL_ADC_GetValue​,也是用于获取转换结果。

特殊功能函数:

  • HAL_ADCEx_ContinuousConversionsStart(ADC_HandleTypeDef* hadc, uint32_t ChannelGroup)​:用于开始连续多通道的ADC转换。
  • HAL_ADCEx_DiscontinuousConversionsStart(ADC_HandleTypeDef* hadc, uint32_t ChannelGroup)​:开始不连续模式下的ADC转换。
  • HAL_ADC_AnalogWDGConfig(ADC_HandleTypeDef* hadc, ADC_AnalogWDGConfTypeDef* AnalogWDGConfig)​:配置模拟看门狗功能。

状态与错误处理:

  • HAL_ADC_GetState(ADC_HandleTypeDef* hadc)​: 获取ADC当前的状态。
  • HAL_ADC_GetError(ADC_HandleTypeDef* hadc)​: 获取ADC的错误代码。

3. 小实验

读取芯片内部的温度,并展示出来。

STM32系列芯片内部存在一个热敏电阻,通过一系列转换,可以得到具体的摄氏度数据,具体方式可见参考手册和数据手册

image

image

先在cube ide中进行配置,勾选内部温度传感器通道

关键代码如下

int main(){
    // 初始化等操作省略
	int AD_Value = 0;
	float Vol_Value = 0.0;
	float Temperature = 0.0;

	for (;;)
	{
	    AD_Value = HAL_ADC_GetValue(&hadc1); // 读取ADC转换数据(12位数据)
	    printf("ADC1_IN16 ADC value: %d\r\n", AD_Value);
	    Vol_Value = AD_Value * (3.3 / 4096); // AD值乘以分辨率即为电压值
	    printf("ADC1_IN16 VOL value: %.2fV\r\n", Vol_Value);
	    Temperature = (1.43 - Vol_Value) / 0.0043 + 25; // 根据公式算出温度值
	    printf("MCU Internal Temperature: %.2f C\r\n", Temperature);
	    printf("\r\n");
	    HAL_Delay(1000);
	}
}

上电有明显升温过程

二、 CAN通信

1. 简介

STM32系列微控制器内置了CAN(Controller Area Network,控制器局域网络)接口,这使得它们非常适合用于需要可靠通信的嵌入式系统,尤其是在汽车、工业控制和其他自动化领域。以下是关于STM32 CAN通信的详细介绍:

CAN总线简介

CAN总线是一种多主、串行通信协议,最初由Bosch公司开发,现已成为ISO国际标准(ISO 11898)。它采用差分信号传输,具备错误检测和处理机制,支持多节点(最多可达110个节点)在同一个网络上进行通信,且具有较高的数据传输可靠性。

STM32的CAN模块特性

  1. 双CAN接口:部分STM32型号(如STM32F103)配备有两个独立的CAN接口,每个接口都可配置为CAN 2.0A或CAN 2.0B协议。
  2. 发送与接收邮箱:通常包含3个发送邮箱和2个接收FIFO(First In First Out)队列,每个FIFO能存储多个报文。发送调度器会根据报文的优先级决定发送顺序。
  3. 位速率与同步:支持不同的位速率(最高可达1Mbps),并能够自动同步到总线上的位速率,适应网络中的不同设备。
  4. 错误处理与恢复:具备强大的错误检测和处理能力,包括位错误、填充错误、CRC错误、形式错误和应答错误等,并支持错误计数及错误主动通知。
  5. 过滤功能:可配置的标准和扩展标识符过滤,允许控制器仅接收感兴趣的报文,减少CPU的中断处理负担。
  6. 远程帧请求:支持发送远程帧以请求其他节点发送特定数据。

初始化与配置

  1. 硬件配置:首先需要正确配置CAN总线的引脚,确保它们被设置为CAN_TX和CAN_RX模式,并连接到物理总线上。还需在总线的两个末端添加终端电阻(通常为120欧姆)以避免信号反射。
  2. 软件配置:使用STM32 HAL库或LL库进行配置,主要包括设置位速率、模式(正常或监听)、激活滤波器、初始化发送和接收邮箱等。
  3. 中断与DMA:可以配置中断或DMA(直接内存访问)来处理接收到的数据或发送完成的通知,以提高效率。

发送与接收数据

  • 发送:将要发送的数据和相关标识符打包成CAN报文格式,然后通过调用相应的函数将报文放入发送邮箱中。CAN控制器会根据优先级自动管理报文发送。
  • 接收:接收到的数据会被自动存入接收FIFO,应用程序通过查询中断或轮询方式检查FIFO,并读取数据。

CAN总线电路

正规的can总线电路是这样的,芯片引脚是CAN_TX和CAN_RX需要一个专门的收发器转为CAN_High和CAN_Low

image

CAN总线通过这种方式表示逻辑0和逻辑1

image

在简易实验中,我们可以用这种等效电路实现不通过收发器实现CAN通信

image

应用实例

在实际应用中,开发者通常会编写代码来初始化CAN接口、配置滤波器、设置中断服务例程(ISR)或DMA传输,并编写发送和接收数据的函数。例如,在汽车电子中,一个节点可能负责发送车速信息,而另一个节点则负责接收并据此调整空调风速。

总之,STM32的CAN通信功能强大且灵活,适合构建复杂的分布式控制系统,通过合理的配置和编程,可以满足大多数工业级和汽车级通信需求。

2. HAL库中的代码

初始化与配置函数

  • HAL_CAN_Init​: 初始化CAN外设。这个函数会配置CAN控制器的时钟、中断、模式等底层硬件设置。
  • HAL_CAN_ConfigFilter​: 配置CAN过滤器,用于指定哪些报文应该被接收。
  • HAL_CAN_ConfigTxTimestamp​: 配置发送报文时的时间戳功能。

发送函数

  • HAL_CAN_AddTxMessage​: 添加报文到发送邮箱准备发送。此函数需要提供报文的标识符、数据、长度和邮箱号。
  • HAL_CAN_Transmit_IT​: 启动CAN报文的中断传输。如果配置了发送邮箱空中断(CAN_IT_TME),当有空闲邮箱时,该函数将触发发送。
  • HAL_CAN_Transmit_DMA​: 使用DMA方式发送CAN报文,适用于大数据量传输。

接收函数

  • HAL_CAN_RxFifo0Callback​: 接收FIFO 0的回调函数,当FIFO 0中有新报文到达时被调用。
  • HAL_CAN_RxFifo1Callback​: 接收FIFO 1的回调函数,类似地,处理FIFO 1的接收事件。
  • HAL_CAN_GetRxMessage​: 从指定的FIFO中获取接收到的报文。

状态查询与控制函数

  • HAL_CAN_GetState​: 获取CAN外设当前的工作状态。
  • HAL_CAN_AbortTxRequest​: 中止指定邮箱的发送请求。
  • HAL_CAN_IsTxMessagePending​: 检查是否有待发送的报文在指定的邮箱中等待。

中断与错误处理

  • HAL_CAN_IRQHandler​: CAN中断处理函数,内部会调用相应的中断回调函数。
  • HAL_CAN_ErrorCallback​: 错误回调函数,当CAN总线发生错误时被调用。
  • HAL_CAN_GetError​: 获取最新的CAN错误状态。

其他辅助函数

  • HAL_CAN_ConfigClockSource​: 配置CAN时钟源。
  • HAL_CAN_DeInit​: 关闭并重置CAN外设。

3. 小实验

2个或以上同学相互连接,利用CAN通信,向对方发送带有本人姓名的信息。连线方式:按基本原理性电路(不带收发器芯片)连接,参考教材图10-1。

由于原书开发版can通信相关参数不明,我没有实现和同学的开发板联机,并且我手头上也没有其他的开发版和其他can通信的相关模块,这个实验我没有用STM32F103C8T6完成,而是借了其他同学的开发板,希望下一届上这个课的同学可以实现这点

int main()
{
    // 其他初始化代码
    for (;;) // for(;;)(开头)
    {
        // (2.1)主循环次数变量+1
        mMainLoopCount++;
        // (2.2)未达到主循环次数设定值,继续循环
        if (mMainLoopCount <= 12889000)
            continue;
        // (2.3)达到主循环次数设定值,执行下列语句,进行灯的亮暗处理
        // (2.3.1)清除循环次数变量
        mMainLoopCount = 0;
        // (2.3.2)如灯状态标志mFlag为'L',灯的闪烁次数+1并显示,改变灯状态及标志
        if (mFlag == 'L') // 判断灯的状态标志
        {
            mLightCount++;
            printf("灯的闪烁次数 mLightCount = %d\n", mLightCount);
            mFlag = 'A';                   // 灯的状态标志
            gpio_set(LIGHT_RED, LIGHT_ON); // 灯“亮”
            printf(" LIGHT_RED:ON--\n");   // 串口输出灯的状态
            // 【***CAN模块发送一帧数据***】
            if (can_send(CAN_1, txMsgID, 9, (uint8_t *)"某某某") != 0)
                printf("failed\r\n");
        }
        // (2.3.3)如灯状态标志mFlag为'A',改变灯状态及标志
        else
        {
            mFlag = 'L';                    // 灯的状态标志
            gpio_set(LIGHT_RED, LIGHT_OFF); // 灯“暗”
            printf(" LIGHT_RED:OFF--\n");   // 串口输出灯的状态
        }
    } // for(;;)结尾
}

图片1

实验结果符合预期

三、 FLASH擦写

1. 简介

STM32系列微控制器内置了非易失性闪存(Flash memory),这是一种在断电后仍能保持数据不丢失的存储器类型。STM32的Flash主要用于存储程序代码、常量数据(如字库、配置表)、以及运行时需要持久化的数据。以下是STM32 Flash的一些关键特性和技术细节:

1. 基本概念

  • 非易失性:断电后数据不会丢失,这一点与RAM(随机存取内存)不同,RAM在断电时会丢失所有数据。
  • 程序存储器:用于存放应用程序的机器码,即用户编写的程序。
  • 系统存储器:通常包含由制造商预置的引导加载程序(Bootloader),用于芯片的初始化和程序的加载。
  • 选项字节存储器:用于存储配置信息,如读保护设置、选择启动模式等。

2. 容量与布局

STM32系列微控制器的Flash容量从几KB到几MB不等,具体取决于不同的型号,详细可参照数据手册。

这是STM32F103C8T6的详细参数:

image

3. 读写操作

  • 读操作:程序执行时,CPU直接从Flash中读取指令和数据。
  • 写操作:包括编程(Programming)和擦除(Erasing)。擦除是将指定区域的内容全部清零(通常是按页擦除),编程则是将数据写入到已擦除的单元中。STM32提供了专用的Flash接口和API,允许用户通过程序控制这些操作,用于数据记录或固件升级等功能。

4. 寿命与耐用性

Flash存储器有一定的擦写寿命,通常以擦写周期计数。STM32的Flash支持数万至数十万次的擦写循环,但具体数值依型号而异。过量的擦写可能导致Flash块损坏,因此在设计应用时需考虑磨损均衡策略。

5. 安全特性

  • 读保护:可以设置选项字节来防止代码被非法读取。
  • 写保护:同样通过设置选项字节,可以防止Flash的某些部分被意外修改。
  • 唯一标识(UID) :STM32 Flash中包含一个唯一的设备标识,可用于防伪和软件授权验证。

6. 编程与擦除机制

  • 擦除:通常以页或扇区为单位进行,STM32的Flash擦除前需要解锁,并在操作完成后重新锁定以确保安全性。
  • 编程:只能对已经擦除过的单元(值为1)写入0,因此编程前必须先擦除目标区域。

7. 应用实例

  • 数据记录:利用Flash存储传感器数据、日志或其他需要长期保存的信息。
  • 固件升级:通过OTA(空中下载技术)或串行通信接口更新应用程序,实现远程维护和功能升级。
  • 配置参数:存储用户的设置或系统配置信息,以便在设备重启后恢复先前状态。

2. HAL库中的代码

解锁/锁定Flash

  • HAL_FLASH_Unlock() ​: 解锁Flash编程和擦除操作。
  • HAL_FLASH_Lock() ​: 锁定Flash编程和擦除操作,以增加系统的安全性。

擦除操作

  • HAL_FLASHEx_Erase(&Flash, &PageError) ​: 擦除指定的Flash页面或整个Flash。Flash​结构体中指定了擦除的起始地址和大小,PageError​用于返回发生错误的页面(如果有的话)。

编程操作

  • HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data) ​: 根据TypeProgram参数指定的数据类型(如半字、全字、双字等)向指定的Flash地址写入数据。例如,要写入16位数据,TypeProgram应设为FLASH_TYPEPROGRAM_HALFWORD​。

读取操作

  • 虽然没有特定的HAL库函数直接命名为“读取Flash”,但你可以直接通过C语言的指针操作来读取Flash中的数据,因为程序代码和常量通常就存储在Flash中,是可直接访问的。

状态检查和错误处理

  • HAL_FLASH_GetError() ​: 获取最近一次Flash操作的错误代码。
  • HAL_FLASH_OperationErrorCallback​: 当Flash操作失败时调用的回调函数,需要用户实现以处理错误。

选项字节操作

  • HAL_FLASH_OB_Unlock() ​: 解锁选项字节以供修改。
  • HAL_FLASH_OB_Lock() ​: 锁定选项字节,防止进一步修改。
  • HAL_FLASHEx_OBProgram() ​: 编程选项字节,用于设置读保护、写保护等。

等待直到Flash操作完成

  • HAL_FLASH_WaitForLastOperation(uint32_t Timeout) ​: 等待直至上一次Flash操作完成或超时。

3. 小实验

写入扇区,扇区号为学号 后2位,数据文本中要有姓名。

/**
 * @brief 将数据写入Flash
 * 
 * 该函数用于将给定的数据写入Flash中指定的地址。首先,它会擦除指定页面,然后将数据以16位字的形式编程到Flash中。
 * 
 * @param addr 写入Flash的起始地址
 * @param data 数据指针,包含要写入Flash的数据
 * @param Size 要写入的数据数量,以16位字为单位
 */
void Flash_WriteData(uint32_t addr, uint16_t *data, uint16_t Size)
{
    // 解锁Flash操作,以允许编程和擦除操作
    HAL_FLASH_Unlock();

    // 初始化擦除结构体,指定擦除类型为页面擦除,擦除地址为给定地址,擦除一个页面
    FLASH_EraseInitTypeDef EraseInitStruct;
    EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
    EraseInitStruct.PageAddress = addr;
    EraseInitStruct.NbPages = 1;

    // 定义一个变量来存储擦除过程中可能出现的错误页面
    uint32_t PageError = 0;

    // 如果擦除操作成功,则进行数据编程
    if (HAL_FLASHEx_Erase(&EraseInitStruct, &PageError) == HAL_OK)
    {
        uint32_t TempBuf = 0;
        // 遍历所有要写入的数据,将每个16位字倒序写入Flash
        for (uint32_t i = 0; i < Size; i++)
        {
            TempBuf = ~(*(data + i));
            TempBuf <<= 16;
            TempBuf += *(data + i);
            HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i * 4, TempBuf);
        }
    }

    // 锁定Flash操作,防止意外修改
    HAL_FLASH_Lock();
}

/**
 * @brief 从Flash中读取数据
 * 
 * 该函数从Flash的指定地址读取数据,并将其存储到提供的数据数组中。函数会检查读取的数据是否完整无误。
 * 
 * @param addr 读取数据的起始地址
 * @param data 数据指针,用于存储读取到的数据
 * @param Size 要读取的数据数量,以16位字为单位
 * @return uint8_t 返回1表示读取成功,返回0表示读取过程中发现数据不一致
 */
uint8_t Flash_ReadData(uint32_t addr, uint16_t *data, uint16_t Size)
{
    uint32_t temp;
    uint8_t result = 1;
    // 遍历所有要读取的数据,检查每个16位字是否符合预期的倒序存储形式
    for (uint32_t i = 0; i < Size; i++)
    {
        temp = *(__IO uint32_t *)(addr + i * 4);
        if ((uint16_t)temp == (uint16_t)(~(temp >> 16)))
        {
            *(data + i) = (uint16_t)temp;
        }
        else
        {
            result = 0;
        }
    }
    return result;
}

int main(void)
{
    // 初始化等操作省略

    printf("\r\nstart\r\n");
    int page0Addr = 0x08000000;
    char *target = "32106100106 luokairui";
    char readbuf[50];

    // 将字符串写入Flash
    Flash_WriteData(page0Addr + 60 * 1024, (uint16_t *)target, strlen(target));

    // 从Flash中读取数据,并检查读取是否成功
    Flash_ReadData(page0Addr + 60 * 1024, (uint16_t *)readbuf, strlen(target));

    // 打印读取到的数据
    printf("\r\n%s\r\n", readbuf);

    // 程序进入无限循环
    while (1);
}

由于我的学号末尾是06,而06要存放elf

所以我改成写入60号扇区

关于作者:

欢迎联系!


评论