7.3 行高:line-height属性[2]

本文探讨了CSS中行高的计算方法与继承特性,包括em、ex和百分比单位的应用,以及不同字体大小对行高显示的影响。同时,介绍了如何避免文字重叠问题,并讨论了图片对行高布局的影响。

7.3.3 行高的计算与继承 以em、ex和百分比为单位的行高,其基数是元素本身的字体尺寸。例如有代码如下:

<p style="font-size:20px;line-height:2em;">字高20px,行高2em。</p> <p style="font-size:30px;line-height:2em;">字高30px,行高2em。</p>
2个段落的行高都为2em,但是字体大小不同,因此显示如图7-23所示。
/web/css/text/img/text_023.gif
  图7-23 行高的计算 行高可以设定得比字体高度小,此时多行的文字将叠加到一起,例如有如下代码,其显示如图7-24所示。
p { font-size : 20px; line-height :10px; } <p>字高20px,行高10px。此时多行的文字将叠加到一起。</p>
/web/css/text/img/text_024.gif
  图7-24 比字体高度小的行高 行高是可继承的,但是继承的是计算值,例如有如下代码:
p { font-size :20px; line-height : 2em; } p span { font-size : 30px; } <p>字高20px。<span>字高30px。</span></p> <p>元素的行高2em,字体尺寸为20px,因此计算值为40px,虽然<span>元素本身的字体尺寸为30px,不过其继承的行高仍为40px。但是在不同的浏览器内显示的效果却不尽相同,如图7-25所示。
/web/css/text/img/text_025.gif
图7-25 行高的不同表现 由于继承的是计算值,因此当元素内的文字字体尺寸不一样的时候,如果设定固定的行高很可能造成字体的重叠,例如有如下代码,其显示如图7-26所示。
p { font-size : 20px; line-height : 1em; } p span { font-size : 30px; } <p>字高20px,行高1em,当文本为多行时可能会发生文字重叠的想象。<span>字高30px。</span></p>
/web/css/text/img/text_026.gif
图7-26行高继承造成文字叠加 为了避免这种情况,可以为每个元素单独定义行高,但是这样很烦琐,因此可以定义一个没有单位的实数值作为缩放因子来统一控制行高,缩放因子是直接继承的,而不是继承计算值。例如修改上例中的行高为:
p { line-height : 1; } 
则上例中的XHTML代码显示如图7-27所示。
/web/css/text/img/text_027.gif
图7-27缩放因子对行高的影响 当内容中含有图片的时候,如果图片的高度大于行高,则含有图片行的行框将被撑开到图片的高度,如图7-28所示。
/web/css/text/img/text_028.gif
图7-28 含有图片的行
注意:图片虽然撑开了行框,但是不会影响行高,因此也不会影响到基于行高来计算的其他属性。 提示:当行内含有图片的时候,图片和文字的垂直对齐方式默认是基线对齐,关于垂直对齐将在本章[7.4 垂直对齐:vertical-align属性]一节中讨论。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>工资条生成工具</title> <!-- 引入SheetJS库用于处理Excel文件 --> <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> <!-- 关键修复:使用支持样式的xlsx-style库替换基础版xlsx库 --> <script src="https://unpkg.com/xlsx-style@0.15.6/dist/xlsx.full.min.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: "Microsoft YaHei", "SimHei", Arial, sans-serif; width: 100%; min-height: 100vh; padding: 20px; font-size: 14px; color: #333; line-height: 1.5; } h1 { text-align: center; margin-bottom: 25px; font-size: 24px; color: #2c3e50; } h2 { font-size: 18px; margin: 15px 0; color: #3498db; } .file-upload { border: 2px dashed #95a5a6; padding: 30px 20px; text-align: center; margin-bottom: 30px; cursor: pointer; border-radius: 6px; transition: all 0.3s ease; background-color: #f8f9fa; } .file-upload:hover, .file-upload.active { border-color: #3498db; background-color: #f1f7fc; } .file-upload p { font-size: 16px; color: #7f8c8d; } #fileInput { display: none; } .controls { display: flex; justify-content: center; margin: 20px 0; } .btn { background-color: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s ease; display: flex; align-items: center; gap: 8px; } .btn:hover { background-color: #2980b9; } .btn:disabled { background-color: #bdc3c7; cursor: not-allowed; } #dataContainer { width: 100%; overflow-x: auto; margin: 0 auto; } table { width: 100%; min-width: 900px; border-collapse: collapse; table-layout: fixed; } th, td { border: 1px solid #ddd; padding: 12px 10px; text-align: left; word-wrap: break-word; word-break: break-all; height: 45px; } /* 数值列右对齐,方便查看 */ td.numeric { text-align: right; } /* 工资条表头样式 */ .salary-slip-header th { background-color: #ecf0f1; color: #2c3e50; font-weight: bold; position: sticky; top: 0; z-index: 2; } /* 合计样式 */ .total-row td { background-color: #f8f9fa; font-weight: bold; } /* 签字样式 */ .signature-row td { background-color: #f9f9f9; } tr:hover { background-color: #f1f7fc; } .message { color: #7f8c8d; text-align: center; padding: 50px 0; font-size: 16px; } .error { color: #e74c3c; } /* 无效数值样式 */ .invalid-value { background-color: #fff3cd; color: #856404; } .date-info { color: #2c3e50; font-size: 15px; margin: 15px 0; padding: 15px 12px; background-color: #f1f9f7; border-left: 3px solid #27ae60; border-radius: 4px; line-height: 1.8; min-height: 45px; } /* 响应式调整 */ @media (max-width: 1200px) { body { padding: 15px; font-size: 13px; } th, td { padding: 10px 8px; height: 40px; } .date-info { padding: 12px 10px; min-height: 40px; } } @media (max-width: 768px) { body { padding: 10px; font-size: 12px; } h1 { font-size: 20px; margin-bottom: 15px; } h2 { font-size: 16px; } .file-upload { padding: 20px 10px; margin-bottom: 20px; } th, td { padding: 8px 6px; height: 36px; } .date-info { padding: 10px 8px; min-height: 36px; } } </style> </head> <body> <h1>工资条生成工具</h1> <!-- 文件上传区域 --> <div class="file-upload" id="dropArea"> <p>点击或拖放工资表Excel文件到这里(支持多次上传更新)</p> <input type="file" id="fileInput" accept=".xlsx, .xls"> </div> <!-- 提取的信息 --> <div id="infoContainer" style="display: none;"> <div class="date-info"> <p><strong>公司名称:</strong><span id="companyName"></span></p> <p><strong>工资月份:</strong><span id="salaryMonth"></span></p> <p><strong>上次更新:</strong><span id="lastUpdated"></span></p> </div> </div> <!-- 控制按钮区域 --> <div class="controls"> <button id="exportBtn" class="btn" disabled> 导出工资条 </button> </div> <!-- 数据展示区域 --> <div id="dataContainer" style="display: none;"> <h2>工资条数据</h2> <table id="dataTable"> <tbody id="dataTableBody"> <!-- 提取的数据将在这里显示,每条数据带独立表头 --> </tbody> </table> </div> <!-- 消息区域 --> <div id="message" class="message">请上传符合格式要求的工资表Excel文件(.xlsx, .xls)</div> <script> // 获取DOM元素 const dropArea = document.getElementById(&#39;dropArea&#39;); const fileInput = document.getElementById(&#39;fileInput&#39;); const dataContainer = document.getElementById(&#39;dataContainer&#39;); const dataTable = document.getElementById(&#39;dataTable&#39;); const dataTableBody = document.getElementById(&#39;dataTableBody&#39;); const message = document.getElementById(&#39;message&#39;); const infoContainer = document.getElementById(&#39;infoContainer&#39;); const companyName = document.getElementById(&#39;companyName&#39;); const salaryMonth = document.getElementById(&#39;salaryMonth&#39;); const lastUpdated = document.getElementById(&#39;lastUpdated&#39;); const exportBtn = document.getElementById(&#39;exportBtn&#39;); // 存储处理后的数据用于导出 let processedDataForExport = null; let currentDate = &#39;&#39;; // 定义预期的表头结构 const expectedHeaders = [ &#39;序号&#39;, &#39;部门&#39;, &#39;姓名&#39;, &#39;出勤&#39;, &#39;基本工资&#39;, &#39;岗位工资&#39;, &#39;绩效&#39;, &#39;浮动津贴&#39;, &#39;扣除&#39;, &#39;应发工资&#39;, &#39;基本养老(个人)&#39;, &#39;失业(个人)&#39;, &#39;基本医疗(个人)&#39;, &#39;大病(个人)&#39;, &#39;公积金(个人)&#39;, &#39;个税&#39;, &#39;实发工资&#39;, &#39;签字&#39; ]; // 定义哪些列是数值类型(索引) const numericColumns = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; // 点击上传区域触发文件选择 dropArea.addEventListener(&#39;click&#39;, () => fileInput.click()); // 文件选择变化时处理 fileInput.addEventListener(&#39;change&#39;, (e) => { if (e.target.files.length > 0) { handleFile(e.target.files[0]); // 重置input值,允许重复选择同一文件 fileInput.value = &#39;&#39;; } }); // 拖放功能增强 dropArea.addEventListener(&#39;dragover&#39;, (e) => { e.preventDefault(); dropArea.classList.add(&#39;active&#39;); }); dropArea.addEventListener(&#39;dragleave&#39;, () => { dropArea.classList.remove(&#39;active&#39;); }); dropArea.addEventListener(&#39;drop&#39;, (e) => { e.preventDefault(); dropArea.classList.remove(&#39;active&#39;); if (e.dataTransfer.files.length > 0) { handleFile(e.dataTransfer.files[0]); } }); // 导出按钮点击事件 exportBtn.addEventListener(&#39;click&#39;, exportToExcel); // 处理Excel文件 - 支持多次上传更新 function handleFile(file) { // 检查文件类型 if (!file.name.match(/\.(xlsx|xls)$/)) { showMessage(&#39;请上传Excel文件(.xlsx, .xls)&#39;, true); return; } showMessage(`正在解析文件: ${file.name}...`); const reader = new FileReader(); reader.onload = function(e) { try { // 读取并解析Excel文件 const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: &#39;array&#39; }); // 获取第一个工作表 const firstSheetName = workbook.SheetNames[1]; const worksheet = workbook.Sheets[firstSheetName]; // 转换为JSON const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); if (jsonData.length < 2) { showMessage(&#39;文件内容不符合要求,至少需要包含标题和表头&#39;, true); return; } // 处理数据并更新表格 processData(jsonData); // 更新最后上传时间 updateLastModifiedTime(); } catch (error) { showMessage(&#39;解析文件失败: &#39; + error.message, true); console.error(error); } }; reader.readAsArrayBuffer(file); } // 更新最后修改时间 function updateLastModifiedTime() { const now = new Date(); const formattedTime = now.toLocaleString(&#39;zh-CN&#39;, { year: &#39;numeric&#39;, month: &#39;2-digit&#39;, day: &#39;2-digit&#39;, hour: &#39;2-digit&#39;, minute: &#39;2-digit&#39;, second: &#39;2-digit&#39; }); lastUpdated.textContent = formattedTime; infoContainer.style.display = &#39;block&#39;; } // 处理数据,提取公司名称和日期并规范化表头 function processData(rawData) { // 提取标题信息(第一数据) let title = rawData[0][0] || &#39;&#39;; // 提取公司名称 const companyMatch = title.match(/^(.*?)\s*\d{4}/); const company = companyMatch ? companyMatch[1] : &#39;未知公司&#39;; // 提取日期 const datePattern = /(\d{4})\D*(\d{1,2})\D*月/; const dateMatch = title.match(datePattern); currentDate = dateMatch ? dateMatch[0] : &#39;&#39;; // 更新公司名称和工资月份信息 companyName.textContent = company; salaryMonth.textContent = currentDate; // 提取特定列数据 const extractedData = extractSpecificData(rawData); // 处理数值数据 const processedData = processNumericData(extractedData); // 保存处理后的数据用于导出 processedDataForExport = processedData; // 启用导出按钮 exportBtn.disabled = false; // 显示处理后的数据(每条数据带独立表头) displayData(processedData, currentDate); } // 提取特定列数据 function extractSpecificData(rawData) { // 转换Excel列字母为索引(A=0, B=1, C=2...) const columnMap = { &#39;A&#39;: 0, &#39;B&#39;: 1, &#39;C&#39;: 2, &#39;O&#39;: 14, &#39;Q&#39;: 16, &#39;R&#39;: 17, &#39;S&#39;: 18, &#39;T&#39;: 19, &#39;W&#39;: 22, &#39;X&#39;: 23, &#39;Y&#39;: 24, &#39;Z&#39;: 25, &#39;AA&#39;: 26, &#39;AB&#39;: 27, &#39;AC&#39;: 28, &#39;AE&#39;: 30, &#39;AG&#39;: 32, &#39;AH&#39;: 33 }; // 需要提取的列索引 const columnsToExtract = Object.values(columnMap); // 找到最后一有数据的索引 let lastDataRow = -1; for (let i = rawData.length - 1; i >= 0; i--) { const row = rawData[i]; if (row && row.some(cell => cell !== undefined && cell !== null && cell !== &#39;&#39;)) { lastDataRow = i; break; } } // 计算范围 const startRow = 5; // 第6(0-based索引) const endRow = lastDataRow !== -1 ? lastDataRow - 6 : -1; // 验证范围有效性 if (lastDataRow === -1) { console.warn(&#39;未找到有数据的&#39;); return []; } if (startRow >= endRow || endRow < 0) { console.warn(`数据范围无效,开始(${startRow+1}) >= 结束(${endRow+1})`); showMessage(`数据范围无效,开始(${startRow+1}) >= 结束(${endRow+1})`, true); return []; } // 提取数据到二维数组 const result = []; for (let i = startRow; i < endRow; i++) { const rowData = rawData[i] || []; const extractedRow = []; columnsToExtract.forEach(colIndex => { extractedRow.push(rowData[colIndex] !== undefined ? rowData[colIndex] : &#39;&#39;); }); result.push(extractedRow); } return result; } // 处理数值数据:校验并保留2位小数(四舍五入),空值不标红 function processNumericData(data) { return data.map(row => { return row.map((cell, index) => { // 检查当前列是否为数值列 if (numericColumns.includes(index)) { // 处理空值情况 if (cell === &#39;&#39; || cell === null || cell === undefined) { return { value: &#39;&#39;, valid: true, // 空值视为有效 rawValue: 0 // 用于计算合计的原始数值 }; } // 尝试转换为数值 const num = parseFloat(cell); // 校验数值有效性 if (isNaN(num)) { return { value: cell, valid: false, rawValue: 0 }; } else { const roundedValue = Math.round(num * 100) / 100; return { value: roundedValue, valid: true, rawValue: roundedValue // 存储用于计算合计的数值 }; } } // 非数值列,直接返回原始值 return { value: cell, valid: true, rawValue: 0 }; }); }); } // 计算各列的总和 function calculateTotals(processedData) { const totals = new Array(expectedHeaders.length).fill(0); processedData.forEach(row => { row.forEach((cell, index) => { // 只对数值列进求和 if (numericColumns.includes(index)) { totals[index] += cell.rawValue; } }); }); // 保留两位小数 return totals.map(total => Math.round(total * 100) / 100); } // 显示解析后的数据 - 每条数据前都添加表头,无间隔,最后添加合计和签字 function displayData(processedData, date) { // 清空表格 dataTableBody.innerHTML = &#39;&#39;; // 如果没有提取到数据,显示提示 if (processedData.length === 0) { const emptyRow = document.createElement(&#39;tr&#39;); const emptyCell = document.createElement(&#39;td&#39;); emptyCell.colSpan = expectedHeaders.length; emptyCell.textContent = &#39;没有提取到符合条件的数据&#39;; emptyRow.appendChild(emptyCell); dataTableBody.appendChild(emptyRow); } else { // 为每条数据添加表头(无间隔) processedData.forEach((row) => { // 添加表头(每条数据前都添加) const headerRow = document.createElement(&#39;tr&#39;); headerRow.className = &#39;salary-slip-header&#39;; expectedHeaders.forEach((header) => { if (header === &#39;应发工资&#39; || header === &#39;实发工资&#39;) { header += `(${date})`; } const th = document.createElement(&#39;th&#39;); th.textContent = header; headerRow.appendChild(th); }); dataTableBody.appendChild(headerRow); // 添加数据 const dataRow = document.createElement(&#39;tr&#39;); row.forEach((cell, cellIndex) => { const td = document.createElement(&#39;td&#39;); td.textContent = cell.value; // 数值列右对齐 if (numericColumns.includes(cellIndex)) { td.classList.add(&#39;numeric&#39;); } // 标记无效数值 if (!cell.valid) { td.classList.add(&#39;invalid-value&#39;); } dataRow.appendChild(td); }); dataTableBody.appendChild(dataRow); }); // 计算各列总和 const totals = calculateTotals(processedData); // 添加合计(倒数第二) const totalRow = document.createElement(&#39;tr&#39;); totalRow.className = &#39;total-row&#39;; // 前3个单元格合并,内容为"合计" const mergedCell = document.createElement(&#39;td&#39;); mergedCell.colSpan = 3; mergedCell.textContent = &#39;合计&#39;; mergedCell.style.textAlign = &#39;center&#39;; totalRow.appendChild(mergedCell); // 第4个单元格为空 const emptyCell = document.createElement(&#39;td&#39;); totalRow.appendChild(emptyCell); // 5-17单元格为各列总和(索引4到16) for (let i = 4; i < expectedHeaders.length; i++) { const td = document.createElement(&#39;td&#39;); td.textContent = totals[i]; if (i == expectedHeaders.length - 1) { td.textContent = &#39;&#39;; } if (numericColumns.includes(i)) { td.classList.add(&#39;numeric&#39;); } totalRow.appendChild(td); } dataTableBody.appendChild(totalRow); // 添加签字(最后一) const signatureRow = document.createElement(&#39;tr&#39;); signatureRow.className = &#39;signature-row&#39;; // 制表 const makerCell = document.createElement(&#39;td&#39;); makerCell.colSpan = 4; makerCell.textContent = &#39;制表:&#39;; signatureRow.appendChild(makerCell); // 财务 const financeCell = document.createElement(&#39;td&#39;); financeCell.colSpan = 4; financeCell.textContent = &#39;财务:&#39;; signatureRow.appendChild(financeCell); // 审核 const auditCell = document.createElement(&#39;td&#39;); auditCell.colSpan = 5; auditCell.textContent = &#39;审核:&#39;; signatureRow.appendChild(auditCell); // 审批 const approveCell = document.createElement(&#39;td&#39;); approveCell.colSpan = 5; approveCell.textContent = &#39;审批:&#39;; signatureRow.appendChild(approveCell); dataTableBody.appendChild(signatureRow); } // 显示数据区域,隐藏消息 dataContainer.style.display = &#39;block&#39;; message.style.display = &#39;none&#39;; } // 导出数据到Excel function exportToExcel() { if (!processedDataForExport || processedDataForExport.length === 0) { showMessage(&#39;没有可导出的数据&#39;, true); return; } // 准备导出数据 const exportData = []; // 添加每条记录的表头和数据 processedDataForExport.forEach(row => { // 添加表头 const headerRow = expectedHeaders.map(header => { if (header === &#39;应发工资&#39; || header === &#39;实发工资&#39;) { return `${header}(${currentDate})`; } return header; }); exportData.push(headerRow); // 添加数据 const dataRow = row.map(cell => cell.value); exportData.push(dataRow); }); // 计算合计 const totals = calculateTotals(processedDataForExport); // 准备合计 const totalRow = []; // 前3个单元格合并为"合计" totalRow.push("合计"); totalRow.push(null); // 第二个单元格(合并后不需要) totalRow.push(null); // 第三个单元格(合并后不需要) totalRow.push(""); // 第四个单元格为空 // 添加5-17列的合计值 for (let i = 4; i < expectedHeaders.length - 1; i++) { totalRow.push(totals[i]); } exportData.push(totalRow); // 准备签字 - 只在第一个单元格设置值,其他为空 const signatureRow = new Array(expectedHeaders.length).fill(null); signatureRow[0] = "制表: 财务: 审核: 审批:"; exportData.push(signatureRow); // 创建工作簿和工作表 const ws = XLSX.utils.aoa_to_sheet(exportData); // 设置单元格合并 if (exportData.length > 0) { const totalRowIndex = exportData.length - 2; // 合计索引 const signatureRowIndex = exportData.length - 1; // 签字索引 ws[&#39;!merges&#39;] = [ // 合计3列合并 { s: { r: totalRowIndex, c: 0 }, e: { r: totalRowIndex, c: 2 } }, // 签字A-R列合并为一个单元格 { s: { r: signatureRowIndex, c: 0 }, e: { r: signatureRowIndex, c: 17 } // 合并到第18列(R列) } ]; } // 设置固定 const range = XLSX.utils.decode_range(ws[&#39;!ref&#39;]); ws[&#39;!rows&#39;] = []; // 初始化配置数组 for (let R = 0; R <= range.e.r; R++) { // 为每一设置固定20 ws[&#39;!rows&#39;][R] = { hpt: 30 }; // hpt单位是1/20点,所以20像素需要乘以20 } // 使用自定义列宽,从第一列开始依次应用 const customWidths = [3.92, 4.8, 5, 4.05, 7.43, 7.68, 3.55, 4.93, 4.18, 12.05, 6.93, 6.93, 7.3, 6.93, 7.55, 5.68, 12.18, 9.43 ]; // 为每个表头应用对应的自定义宽度 ws[&#39;!cols&#39;] = customWidths.map(width => ({ // 保留一位小数并转换为数字,确保宽度值正确 wch: parseFloat(width.toFixed(2)) })); // 定义包含自动换的单元格样式 const cellStyle = { font: { sz: 10 // 小一号字体 }, alignment: { wrapText: true // 核心配置:启用自动换 } }; // 应用样式到所有单元格 for (const cellAddress in ws) { // 跳过非单元格属性(如!cols、!rows等) if (cellAddress.startsWith(&#39;!&#39;)) continue; if (ws[cellAddress]) { // 合并已有样式与自动换样式 ws[cellAddress].s = { ...ws[cellAddress].s, ...cellStyle }; } else { // 为空白单元格设置样式 ws[cellAddress] = { s: cellStyle }; } } // 创建工作簿并添加工作表 const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "工资条"); // 生成文件名(包含公司名称和日期) const company = companyName.textContent || "未知公司"; const fileName = `${company}_${currentDate || new Date().toLocaleDateString()}_工资条.xlsx`; // 导出文件 XLSX.writeFile(wb, fileName); } // 显示消息 function showMessage(text, isError = false) { message.textContent = text; message.className = isError ? &#39;message error&#39; : &#39;message&#39;; message.style.display = &#39;block&#39;; dataContainer.style.display = &#39;none&#39;; infoContainer.style.display = &#39;none&#39;; } </script> </body> </html> 希望导出后有内容部分加边框,字体为宋体10号字,自动换,对其方式上下左右居中,奇数加粗
08-07
module st7789_init ( input clk, // 5MHz时钟 input rst_n, // 异步复位(低有效) input locked, // PLL锁定信号 input sda_end, // SPI传输完成(有效) input spi_busy, // SPI忙(有效) output reg sda_start, // 启动SPI传输(脉冲) output reg [8:0] data_out,// 9位数据(bit8:命令/数据标志) output reg initial_end, // 初始化完成(有效) output reg [4:0] cnt_index// 步骤计数器(观察用) ); // -------------------------- // 1. 核心参数配置 // -------------------------- localparam SCREEN_WIDTH = 10&#39;d240; // 列数(0~239) localparam SCREEN_HEIGHT = 10&#39;d320; // (0~319) localparam PIXEL_BYTES = 2&#39;d2; // RGB565(2字节/像素) localparam GRAM_TOTAL = 18&#39;d153600; // 总字节数(240*320*2) // 颜色与线宽 localparam WAVE_COLOR = 16&#39;hF800; // 红色 localparam BG_COLOR = 16&#39;h0000; // 黑色 localparam LINE_WIDTH = 3&#39;d5; // 线宽±1 // 滚动参数 localparam SCROLL_SPEED = 20&#39;d500_000; // 0.1秒/列(5MHz时钟) // -------------------------- // 2. 内部寄存器(gram_cnt只在这里声明,不重复声明) // -------------------------- reg [1:0] state; reg [23:0] delay_cnt; reg [17:0] gram_cnt; // 字节计数器(仅由状态机驱动) reg [9:0] current_row; // 当前 reg [9:0] current_col; // 当前列 wire [8:0] rom_9bit_data; // ROM输出数据 // 滚动相关寄存器 reg [7:0] scroll_offset; // 滚动偏移量 reg [19:0] scroll_timer; // 滚动定时器 wire [7:0] rom_addr = (current_col + 50) % SCREEN_WIDTH; // -------------------------- // 3. 滚动逻辑(只驱动scroll_offset,不碰gram_cnt) // -------------------------- always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scroll_offset <= 8&#39;d0; scroll_timer <= 20&#39;d0; end else if (initial_end) begin // 初始化完成后才滚动 if (scroll_timer >= SCROLL_SPEED) begin scroll_timer <= 20&#39;d0; // 偏移量回环(向左滚动:递增;向右:递减) scroll_offset <=scroll_offset + 8&#39;d1; end else begin scroll_timer <= scroll_timer + 20&#39;d1; end end end // -------------------------- // 4. ROM例化(带滚动偏移的地址) // -------------------------- rom_test U_rom_9bit ( .addr (rom_addr), // 偏移地址 .clk (clk), .rst (rst_n), .rd_data (rom_9bit_data) ); // -------------------------- // 5. 列号计算(仅基于gram_cnt,不驱动gram_cnt) // -------------------------- always @(posedge clk or negedge rst_n) begin if (!rst_n) begin current_row <= 10&#39;d0; current_col <= 10&#39;d0; end else begin // 号 = 总字节数 / 每字节数(每240列*2字节=480字节) current_row <= gram_cnt / (SCREEN_WIDTH * PIXEL_BYTES); // 列号 = 像素序号 % 列数(像素序号=总字节数/2) current_col <= (gram_cnt / PIXEL_BYTES) % SCREEN_WIDTH; end end // -------------------------- // 6. 波形点判断(不变) // -------------------------- wire [8:0] final_y_data = rom_9bit_data + 9&#39;d32; // 垂直居中偏移 wire [9:0] wave_min = (final_y_data >= LINE_WIDTH) ? (final_y_data - LINE_WIDTH) : 10&#39;d0; wire [9:0] wave_max = final_y_data + LINE_WIDTH; wire is_wave_pixel = (current_row >= wave_min) && (current_row <= wave_max); // -------------------------- // 7. 数据输出(不变) // -------------------------- always @(*) begin case (cnt_index) // 初始化命令 0 : data_out = 9&#39;h001; // 软件复位 1 : data_out = 9&#39;h011; // 退出睡眠 2 : data_out = 9&#39;h03A; // 像素格式 3 : data_out = 9&#39;h105; // RGB565 4 : data_out = 9&#39;h029; // 开启显示 5 : data_out = 9&#39;h02A; // 列地址设置 6 : data_out = 9&#39;h100; // 列起始8位 7 : data_out = 9&#39;h100; // 列起始低8位 8 : data_out = 9&#39;h100; // 列结束8位 9 : data_out = 9&#39;h1EF; // 列结束低8位(239) 10 : data_out = 9&#39;h02B; // 地址设置 11 : data_out = 9&#39;h100; // 起始8位 12 : data_out = 9&#39;h100; // 起始低8位 13 : data_out = 9&#39;h101; // 结束8位 14 : data_out = 9&#39;h13F; // 结束低8位(319) 15 : data_out = 9&#39;h02C; // 开始写入GRAM // 像素数据(RGB565/低字节) 16 : data_out = {1&#39;b1, is_wave_pixel ? WAVE_COLOR[15:8] : BG_COLOR[15:8]}; 17 : data_out = {1&#39;b1, is_wave_pixel ? WAVE_COLOR[7:0] : BG_COLOR[7:0]}; default: data_out = 9&#39;h000; endcase end // -------------------------- // 8. 主状态机(唯一驱动gram_cnt的地方,修复核心) // -------------------------- always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= 2&#39;b00; sda_start <= 1&#39;b0; initial_end <= 1&#39;b0; cnt_index <= 5&#39;d0; delay_cnt <= 24&#39;d0; gram_cnt <= 18&#39;d0; // 仅在这里初始化gram_cnt end else if (!locked) begin state <= 2&#39;b00; cnt_index <= 5&#39;d0; initial_end <= 1&#39;b0; gram_cnt <= 18&#39;d0; end else begin case (state) 2&#39;b00: begin // IDLE状态:准备发送 sda_start <= 1&#39;b0; if (!initial_end) begin // 初始化阶段:先发命令,再发GRAM数据 if (cnt_index <= 15) begin // 命令需要延迟(复位/睡眠命令延迟更长) delay_cnt <= (cnt_index == 0) ? 24&#39;d1_000_000 : // 复位延迟1ms (cnt_index == 1) ? 24&#39;d500_000 : // 退出睡眠延迟500us 24&#39;d1000; // 其他命令延迟1us state <= 2&#39;b11; // 进入延迟状态 end else begin state <= 2&#39;b01; // 命令发完,准备发像素数据 end end else begin // 持续刷新阶段:直接准备发像素数据 state <= 2&#39;b01; end end 2&#39;b01: begin // SEND状态:启动SPI传输 sda_start <= 1&#39;b1; // 生成SPI启动脉冲 state <= 2&#39;b10; // 进入等待传输完成状态 end 2&#39;b10: begin // WAIT_END状态:等待SPI传输完成 sda_start <= 1&#39;b0; // 撤销启动脉冲 if (sda_end && !spi_busy) begin // 传输完成且SPI空闲 if (!initial_end) begin // 初始化阶段的逻辑 if (cnt_index < 5&#39;d15) begin // 命令还没发完,继续发下一条命令 cnt_index <= cnt_index + 5&#39;d1; state <= 2&#39;b00; end else if (cnt_index == 15) begin // 刚发完“开始写入GRAM”命令,切换到像素数据阶段 cnt_index <= 5&#39;d16; state <= 2&#39;b00; end else if (cnt_index == 16 || cnt_index == 17) begin // 正在发像素数据:更新gram_cnt if (gram_cnt < GRAM_TOTAL - 1) begin gram_cnt <= gram_cnt + 1&#39;b1; // 字节计数器累加 // 交替发送RGB字节(16)和低字节(17) cnt_index <= (cnt_index == 16) ? 5&#39;d17 : 5&#39;d16; state <= 2&#39;b01; // 继续发下一个字节 end else begin // 1次GRAM写满,初始化完成 initial_end <= 1&#39;b1; gram_cnt <= 18&#39;d0; // 重置计数器,为刷新做准备 cnt_index <= 5&#39;d16; // 下次直接发像素数据 end end end else begin // 持续刷新阶段的逻辑:循环发像素数据 if (gram_cnt < GRAM_TOTAL - 1) begin gram_cnt <= gram_cnt + 1&#39;b1; // 字节计数器累加 cnt_index <= (cnt_index == 16) ? 5&#39;d17 : 5&#39;d16; state <= 2&#39;b01; // 继续发下一个字节 end else begin gram_cnt <= 18&#39;d0; // 写满后重置,循环刷新 cnt_index <= 5&#39;d16; state <= 2&#39;b01; end end end end 2&#39;b11: begin // DELAY状态:命令延迟等待 if (delay_cnt > 0) begin delay_cnt <= delay_cnt - 1&#39;b1; end else begin state <= 2&#39;b01; // 延迟结束,启动SPI传输 end end endcase end end endmodule
最新发布
10-10
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值